I'm new to Ruby and trying to solve a problem. I have an array of hashes:
list = [{"amount"=>2.25,"rel_id"=>1103, "date"=>"2012-12-21"},
{"amount"=>2.75,"rel_id"=>1103, "date"=>"2012-12-24"},
{"amount"=>2.85,"rel_id"=>666, "date"=>"2012-12-27"},
{"amount"=>3.15,"rel_id"=>666, "date"=>"2012-12-28"}
#and many many more..
]
I need to group them by rel_id, that i could see total amount and dates they were given, in this kind of format:
{1103=>{:total_amount=>5.0, :dates=>["2012-12-21", "2012-12-24"]}, 666=>{:total_amount=>6.0, :dates=>["2012-12-27", "2012-12-28"]}}
I solved this in this way, but i'm pretty sure it's one of the worst approach to do that and i think it's not a ruby way..
results = {}
list.each do |line|
if !(results.has_key?(line["rel_id"]))
results[line["rel_id"]]={:total_amount=>line["amount"],:dates=>[line["date"]]}
else
results[line["rel_id"]][:total_amount] = results[line["rel_id"]][:total_amount]+line["amount"]
results[line["rel_id"]][:dates]<<line["date"]
end
end
Maybe you could give me or explain how to implement a nicer, more beautiful approach in a ruby way?
You can do something like this:
list.each_with_object({}) do |details, rollup|
rollup[details["rel_id"]] ||= { total_amount: 0, dates: [] }
rollup[details["rel_id"]][:total_amount] += details["amount"]
rollup[details["rel_id"]][:dates] << details["date"]
end
Edited for readability/names.
Functional approach (I'll use mash, use Hash[...] if no Facets):
purchases_grouped = list.group_by { |p| p["rel_id"] }
result = purchases_grouped.mash do |rel_id, purchases|
total_amount = purchases.map { |p| p["amount"] }.reduce(:+)
dates = purchases.map { |p| p["date"] }
accumulated = {total_amount: total_amount, dates: dates}
[rel_id, accumulated]
end
#=> {1103=>{:total_amount=>5.0, :dates=>["2012-12-21", "2012-12-24"]},
# 666 =>{:total_amount=>6.0, :dates=>["2012-12-27", "2012-12-28"]}}
h = list.group_by{|h| h["rel_id"]}
h.each{|k, v| h[k] = {
total_amount: v.inject(0){|x, h| x + h["amount"]},
dates: v.map{|h| h["date"]},
}}
h # => ...
Or
h = list.group_by{|h| h["rel_id"]}
h.each{|k, v| h[k] = {
total_amount: v.map{|h| h["amount"]}.inject(:+),
dates: v.map{|h| h["date"]},
}}
h # => ...
list = [
{amount: 2.25, rel_id: 1103, date: "2012-12-21"},
{amount: 2.75, rel_id: 1103, date: "2012-12-24"},
{amount: 2.85, rel_id: 666, date: "2012-12-27"},
{amount: 3.15, rel_id: 666, date: "2012-12-28"},
]
results = Hash.new do |hash, key|
hash[key] = {}
end
list.each do |hash|
totals = results[hash[:rel_id]]
totals[:amount] ||= 0
totals[:amount] += hash[:amount]
totals[:dates] ||= []
totals[:dates] << hash[:date]
end
p results
--output:--
{1103=>{:amount=>5.0, :dates=>["2012-12-21", "2012-12-24"]},
666=>{:amount=>6.0, :dates=>["2012-12-27", "2012-12-28"]}}
Alex Peachey's each_with_object solution modified:
results = list.each_with_object({}) do |h, acc|
record = acc[h["rel_id"]]
record ||= { total_amount: 0, dates: [] }
record[:total_amount] += h["amount"]
record[:dates] << h["date"]
end
Related
Given I have this hash:
h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
And I convert to OpenStruct:
o = OpenStruct.new(h)
=> #<OpenStruct a="a", b="b", c={:d=>"d", :e=>"e"}>
o.a
=> "a"
o.b
=> "b"
o.c
=> {:d=>"d", :e=>"e"}
2.1.2 :006 > o.c.d
NoMethodError: undefined method `d' for {:d=>"d", :e=>"e"}:Hash
I want all the nested keys to be methods as well. So I can access d as such:
o.c.d
=> "d"
How can I achieve this?
You can monkey-patch the Hash class
class Hash
def to_o
JSON.parse to_json, object_class: OpenStruct
end
end
then you can say
h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
o = h.to_o
o.c.d # => 'd'
See Convert a complex nested hash to an object.
I came up with this solution:
h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
json = h.to_json
=> "{\"a\":\"a\",\"b\":\"b\",\"c\":{\"d\":\"d\",\"e\":\"e\"}}"
object = JSON.parse(json, object_class:OpenStruct)
object.c.d
=> "d"
So for this to work, I had to do an extra step: convert it to json.
personally I use the recursive-open-struct gem - it's then as simple as RecursiveOpenStruct.new(<nested_hash>)
But for the sake of recursion practice, I'll show you a fresh solution:
require 'ostruct'
def to_recursive_ostruct(hash)
result = hash.each_with_object({}) do |(key, val), memo|
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
end
OpenStruct.new(result)
end
puts to_recursive_ostruct(a: { b: 1}).a.b
# => 1
edit
Weihang Jian showed a slight improvement to this here https://stackoverflow.com/a/69311716/2981429
def to_recursive_ostruct(hash)
hash.each_with_object(OpenStruct.new) do |(key, val), memo|
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
end
end
Also see https://stackoverflow.com/a/63264908/2981429 which shows how to handle arrays
note
the reason this is better than the JSON-based solutions is because you can lose some data when you convert to JSON. For example if you convert a Time object to JSON and then parse it, it will be a string. There are many other examples of this:
class Foo; end
JSON.parse({obj: Foo.new}.to_json)["obj"]
# => "#<Foo:0x00007fc8720198b0>"
yeah ... not super useful. You've completely lost your reference to the actual instance.
Here's a recursive solution that avoids converting the hash to json:
def to_o(obj)
if obj.is_a?(Hash)
return OpenStruct.new(obj.map{ |key, val| [ key, to_o(val) ] }.to_h)
elsif obj.is_a?(Array)
return obj.map{ |o| to_o(o) }
else # Assumed to be a primitive value
return obj
end
end
My solution is cleaner and faster than #max-pleaner's.
I don't actually know why but I don't instance extra Hash objects:
def dot_access(hash)
hash.each_with_object(OpenStruct.new) do |(key, value), struct|
struct[key] = value.is_a?(Hash) ? dot_access(value) : value
end
end
Here is the benchmark for you reference:
require 'ostruct'
def dot_access(hash)
hash.each_with_object(OpenStruct.new) do |(key, value), struct|
struct[key] = value.is_a?(Hash) ? dot_access(value) : value
end
end
def to_recursive_ostruct(hash)
result = hash.each_with_object({}) do |(key, val), memo|
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
end
OpenStruct.new(result)
end
require 'benchmark/ips'
Benchmark.ips do |x|
hash = { a: 1, b: 2, c: { d: 3 } }
x.report('dot_access') { dot_access(hash) }
x.report('to_recursive_ostruct') { to_recursive_ostruct(hash) }
end
Warming up --------------------------------------
dot_access 4.843k i/100ms
to_recursive_ostruct 5.218k i/100ms
Calculating -------------------------------------
dot_access 51.976k (± 5.0%) i/s - 261.522k in 5.044482s
to_recursive_ostruct 50.122k (± 4.6%) i/s - 250.464k in 5.008116s
My solution, based on max pleaner's answer and similar to Xavi's answer:
require 'ostruct'
def initialize_open_struct_deeply(value)
case value
when Hash
OpenStruct.new(value.transform_values { |hash_value| send __method__, hash_value })
when Array
value.map { |element| send __method__, element }
else
value
end
end
Here is one way to override the initializer so you can do OpenStruct.new({ a: "b", c: { d: "e", f: ["g", "h", "i"] }}).
Further, this class is included when you require 'json', so be sure to do this patch after the require.
class OpenStruct
def initialize(hash = nil)
#table = {}
if hash
hash.each_pair do |k, v|
self[k] = v.is_a?(Hash) ? OpenStruct.new(v) : v
end
end
end
def keys
#table.keys.map{|k| k.to_s}
end
end
Basing a conversion on OpenStruct works fine until it doesn't. For instance, none of the other answers here properly handle these simple hashes:
people = { person1: { display: { first: 'John' } } }
creds = { oauth: { trust: true }, basic: { trust: false } }
The method below works with those hashes, modifying the input hash rather than returning a new object.
def add_indifferent_access!(hash)
hash.each_pair do |k, v|
hash.instance_variable_set("##{k}", v.tap { |v| send(__method__, v) if v.is_a?(Hash) } )
hash.define_singleton_method(k, proc { hash.instance_variable_get("##{k}") } )
end
end
then
add_indifferent_access!(people)
people.person1.display.first # => 'John'
Or if your context calls for a more inline call structure:
creds.yield_self(&method(:add_indifferent_access!)).oauth.trust # => true
Alternatively, you could mix it in:
module HashExtension
def very_indifferent_access!
each_pair do |k, v|
instance_variable_set("##{k}", v.tap { |v| v.extend(HashExtension) && v.send(__method__) if v.is_a?(Hash) } )
define_singleton_method(k, proc { self.instance_variable_get("##{k}") } )
end
end
end
and apply to individual hashes:
favs = { song1: { title: 'John and Marsha', author: 'Stan Freberg' } }
favs.extend(HashExtension).very_indifferent_access!
favs.song1.title
Here is a variation for monkey-patching Hash, should you opt to do so:
class Hash
def with_very_indifferent_access!
each_pair do |k, v|
instance_variable_set("##{k}", v.tap { |v| v.send(__method__) if v.is_a?(Hash) } )
define_singleton_method(k, proc { instance_variable_get("##{k}") } )
end
end
end
# Note the omission of "v.extend(HashExtension)" vs. the mix-in variation.
Comments to other answers expressed a desire to retain class types. This solution accommodates that.
people = { person1: { created_at: Time.now } }
people.with_very_indifferent_access!
people.person1.created_at.class # => Time
Whatever solution you choose, I recommend testing with this hash:
people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }
If you are ok with monkey-patching the Hash class, you can do:
require 'ostruct'
module Structurizable
def each_pair(&block)
each do |k, v|
v = OpenStruct.new(v) if v.is_a? Hash
yield k, v
end
end
end
Hash.prepend Structurizable
people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }
puts OpenStruct.new(people).person1.display.first
Ideally, instead of pretending this, we should be able to use a Refinement, but for some reason I can't understand it didn't worked for the each_pair method (also, unfortunately Refinements are still pretty limited)
I have an array with hashes in it. If they have the same key I just want to add its value.
#receivers << result
#receivers
=> [{:email=>"user_02#yorlook.com", :amount=>10.00}]
result
=> {:email=>"user_02#yorlook.com", :amount=>7.00}
I want the result of above to look like this
[{:email=>"user_02#yorlook.com", :amount=>17.00}]
Does anyone know how to do this?
Here is the the entire method
def receivers
#receivers = []
orders.each do |order|
product_email = order.product.user.paypal_email
outfit_email = order.outfit_user.paypal_email
if order.user_owns_outfit?
result = { email: product_email, amount: amount(order.total_price) }
else
result = { email: product_email, amount: amount(order.total_price, 0.9),
email: outfit_email, amount: amount(order.total_price, 0.1) }
end
#receivers << result
end
end
Using Enumerable#group_by
#receivers.group_by {|h| h[:email]}.map do |k, v|
{email: k, amount: v.inject(0){|s,h| s + h[:amount] } }
end
# => [{:email=>"user_02#yorlook.com", :amount=>17.0}]
Using Enumerable#each_with_object
#receivers.each_with_object(Hash.new(0)) {|h, nh| nh[h[:email]]+= h[:amount] }.map do |k, v|
{email: k, amount: v}
end
# Output: [{ "em#il.one" => 29.0 }, { "em#il.two" => 39.0 }]
def receivers
return #receivers if #receivers
# Produces: { "em#il.one" => 29.0, "em#il.two" => 39.0 }
partial_result = orders.reduce Hash.new(0.00) do |result, order|
product_email = order.product.user.paypal_email
outfit_email = order.outfit_user.paypal_email
if order.user_owns_outfit?
result[product_email] += amount(order.total_price)
else
result[product_email] += amount(order.total_price, .9)
result[outfit_email] += amount(order.total_price, .1)
end
result
end
#receivers = partial_result.reduce [] do |result, (email, amount)|
result << { email => amount }
end
end
I would just write the code this way:
def add(destination, source)
if destination.nil?
return nil
end
if source.class == Hash
source = [source]
end
for item in source
target = destination.find {|d| d[:email] == item[:email]}
if target.nil?
destination << item
else
target[:amount] += item[:amount]
end
end
destination
end
usage:
#receivers = []
add(#receivers, {:email=>"user_02#yorlook.com", :amount=>10.00})
=> [{:email=>"user_02#yorlook.com", :amount=>10.0}]
add(#receivers, #receivers)
=> [{:email=>"user_02#yorlook.com", :amount=>20.0}]
a = [
{:email=>"user_02#yorlook.com", :amount=>10.0},
{:email=>"user_02#yorlook.com", :amount=>7.0}
]
a.group_by { |v| v.delete :email } # group by emails
.map { |k, v| [k, v.inject(0) { |memo, a| memo + a[:amount] } ] } # sum amounts
.map { |e| %i|email amount|.zip e } # zip to keys
.map &:to_h # convert nested arrays to hashes
From what I understand, you could get away with just .inject:
a = [{:email=>"user_02#yorlook.com", :amount=>10.00}]
b = {:email=>"user_02#yorlook.com", :amount=>7.00}
c = {email: 'user_03#yorlook.com', amount: 10}
[a, b, c].flatten.inject({}) do |a, e|
a[e[:email]] ||= 0
a[e[:email]] += e[:amount]
a
end
=> {
"user_02#yorlook.com" => 17.0,
"user_03#yorlook.com" => 10
}
a beginner question here, please help me out
my_array = ["city1:state1","city2:state2","city3:state1","city4:state3","city5:state1"]
from this array how can i make a hash like this?
{
state1: [city1,city3,city5],
state2: [city2],
state3: [city4]
}
i am trying this way
my_hash = { }
my_array.each do |cs|
temp = cs.split(":")
if my_hash.keys.include? temp[1]
my_hash[temp[1]] = temp[0]
else
my_hash[temp[1]] = temp[0]
end
end
but i am not getting how to match keys of my hash and append to keys.
A little modification can work:
my_hash = { }
my_array.each do |cs|
temp = cs.split(":")
if my_hash.keys.include? temp[1].to_sym
my_hash[temp[1].to_sym] << temp[0]
else
my_hash[temp[1].to_sym] = [temp[0]]
end
end
Result is {:state1=>["city1", "city3", "city5"], :state2=>["city2"], :state3=>["city4"]}. I'm assuming this is what you mean (the keys are symbols, and the values are arrays of strings).
You can use Hash defaults to achieve an alternative solution:
my_array = ["city1:state1","city2:state2","city3:state1","city4:state3","city5:state1"]
hash = Hash.new do |hash, key|
hash[key] = []
end
my_array.each_with_object(hash) do |string, hash|
city, state = string.split(":")
hash[state.to_sym] << city
end
# => {:state1=>["city1", "city3", "city5"], :state2=>["city2"], :state3=>["city4"]}
Try this(considering that you mistyped state3:[city3] instead of state3:[city4] in your question):
my_array = ["city1:state1","city2:state2","city3:state1","city4:state3","city5:state1"]
my_hash = { }
my_array.each do |cs|
value, key = cs.split(":")
key = key.to_sym
if my_hash[key].present?
my_hash[key] << value
else
my_hash[key] = [value]
end
end
my_hash #=> {:state1=>["city1", "city3", "city5"], :state2=>["city2"], :state3=>["city4"]}
Or, one-liner:
my_hash = my_array.inject({}){|h, cs| value, key = cs.split(":"); key = key.to_sym; h[key].present? ? (h[key] << value) : h[key] = [value]; h }
my_hash #=> {:state1=>["city1", "city3", "city5"], :state2=>["city2"], :state3=>["city4"]}
or even better(based on jesper's idea of Hash):
my_array.inject(Hash.new{|h,k| h[k] = [] }){ |my_hash, cs| value, key = cs.split(":"); my_hash[key.to_sym] << value; my_hash }
my_hash #=> {:state1=>["city1", "city3", "city5"], :state2=>["city2"], :state3=>["city4"]}
is there any inbuilt function in the Ruby String class that can give me all the prefixes of a string in Ruby. Something like:
"ruby".all_prefixes => ["ruby", "rub", "ru", "r"]
Currently I have made a custom function for this:
def all_prefixes search_string
dup_string = search_string.dup
return_list = []
while(dup_string.length != 0)
return_list << dup_string.dup
dup_string.chop!
end
return_list
end
But I am looking for something more rubylike, less code and something magical.
Note: of course it goes without saying original_string should remain as it is.
No, there is no built-in method for this. You could do it like this:
def all_prefixes(string)
string.size.times.collect { |i| string[0..i] }
end
all_prefixes('ruby')
# => ["r", "ru", "rub", "ruby"]
A quick benchmark:
require 'fruity'
string = 'ruby'
compare do
toro2k do
string.size.times.collect { |i| string[0..i] }
end
marek_lipka do
(0...(string.length)).map{ |i| string[0..i] }
end
jorg_w_mittag do
string.chars.inject([[], '']) { |(res, memo), c|
[res << memo += c, memo]
}.first
end
jorg_w_mittag_2 do
acc = ''
string.chars.map {|c| acc += c }
end
stefan do
Array.new(string.size) { |i| string[0..i] }
end
end
And the winner is:
Running each test 512 times. Test will take about 1 second.
jorg_w_mittag_2 is faster than stefan by 19.999999999999996% ± 10.0%
stefan is faster than marek_lipka by 10.000000000000009% ± 10.0%
marek_lipka is faster than jorg_w_mittag by 10.000000000000009% ± 1.0%
jorg_w_mittag is similar to toro2k
def all_prefixes(str)
acc = ''
str.chars.map {|c| acc += c }
end
What about
str = "ruby"
prefixes = Array.new(str.size) { |i| str[0..i] } #=> ["r", "ru", "rub", "ruby"]
This is maybe a long shot, but if you want to find distinct abbreviations for a set of strings, you can use the Abbrev module:
require 'abbrev'
Abbrev.abbrev(['ruby']).keys
=> ["rub", "ru", "r", "ruby"]
A little bit shorter form:
def all_prefixes(search_string)
(0...(search_string.length)).map{ |i| search_string[0..i] }
end
all_prefixes 'ruby'
# => ["r", "ru", "rub", "ruby"]
def all_prefixes(str)
str.chars.inject([[], '']) {|(res, memo), c| [res << memo += c, memo] }.first
end
str = "ruby"
prefixes = str.size.times.map { |i| str[0..i] } #=> ["r", "ru", "rub", "ruby"]
Two not mentioned before and faster than those in the #toro2k's accepted comparison answer.
(1..s.size).map { |i| s[0, i] }
=> ["r", "ru", "rub", "ruby"]
Array.new(s.size) { |i| s[0, i+1] }
=> ["r", "ru", "rub", "ruby"]
Strangely, nobody used String#[start, length] before, only the slower String#[range].
And I think at least my first solution is quite straightforward.
Benchmark results (using Ruby 2.4.2):
user system total real
toro2k 14.594000 0.000000 14.594000 ( 14.724630)
marek_lipka 12.485000 0.000000 12.485000 ( 12.635404)
jorg_w_mittag 16.968000 0.000000 16.968000 ( 17.080315)
jorg_w_mittag_2 11.828000 0.000000 11.828000 ( 11.935078)
stefan 10.766000 0.000000 10.766000 ( 10.831517)
stefanpochmann 9.734000 0.000000 9.734000 ( 9.765227)
stefanpochmann 2 8.219000 0.000000 8.219000 ( 8.240854)
My benchmark code:
require 'benchmark'
string = 'ruby'
#n = 10**7
Benchmark.bm(20) do |x|
#x = x
def report(name, &block)
#x.report(name) {
#n.times(&block)
}
end
report('toro2k') {
string.size.times.collect { |i| string[0..i] }
}
report('marek_lipka') {
(0...(string.length)).map{ |i| string[0..i] }
}
report('jorg_w_mittag') {
string.chars.inject([[], '']) { |(res, memo), c|
[res << memo += c, memo]
}.first
}
report('jorg_w_mittag_2') {
acc = ''
string.chars.map {|c| acc += c }
}
report('stefan') {
Array.new(string.size) { |i| string[0..i] }
}
report('stefanpochmann') {
(1..string.size).map { |i| string[0, i] }
}
report('stefanpochmann 2') {
Array.new(string.size) { |i| string[0, i+1] }
}
end
I have a string like this,
str = "uu#p, xx#m, yy#n, zz#m"
I want to know how to convert the given string into a hash. (i.e my actual requirement is, how many values (before the # symbol) have the m, n and p. I don't want the counting, I need an exact value). The output would be better like this,
{"m" => ["xx", "zz"], "n" => ["yy"], "p" => ["uu"]}
Can help me anyone, please?
Direct copy/past of an IRB session:
>> str.split(/, /).inject(Hash.new{|h,k|h[k]=[]}) do |h, s|
.. v,k = s.split(/#/)
.. h[k] << v
.. h
.. end
=> {"p"=>["uu"], "m"=>["xx", "zz"], "n"=>["yy"]}
Simpler code for a newbie :)
str = "uu#p, xx#m, yy#n, zz#m"
h = {}
str.split(",").each do |x|
v,k = x.split('#')
h[k] ||= []
h[k].push(v)
end
p h
FP style:
grouped = str
.split(", ")
.group_by { |s| s.split("#")[1] }
.transform_values { |ss| ss.map { |x| s.split("#")[0] } }
#=> {"m"=>["xx", "zz"], "n"=>["yy"], "p"=>["uu"]}
This is a pretty common pattern. Using Facets.map_by:
require 'facets'
str.split(", ").map_by { |s| s.split("#", 2).reverse }
#=> {"m"=>["xx", "zz"], "n"=>["yy"], "p"=>["uu"]}