Merging & Summing nested hashes in Ruby - ruby-on-rails

What I'm trying to do is very similar to the question outlined in this post, but I have one additional problem in that the nested values of my hash need to have their dates grouped and the values of each date summed. The goal is to create a Multiple Series Graph in Chartkick.
The query, grabbing a month range for example:
arr = LineItem.includes(:order, :product)
.where(orders: {order_date: Date.parse("Jan 1 2020")..Date.parse("Feb 1 2020")})
.map { |line_item| { name: line_item.product.model_number, data: { line_item.order.order_date.strftime('%a %b %d, %Y') => line_item.order_quantity } } }
The output hash:
=> [
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>5}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>1}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP02", :data=>{"Wed Jan 22, 2020"=>1}},
{:name=>"FR-GP04", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>4}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>6}},
{:name=>"FR-GP04", :data=>{"Wed Jan 22, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Tue Jan 21, 2020"=>5}},
{:name=>"FR-GP01", :data=>{"Thu Jan 23, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Thu Jan 23, 2020"=>1}},
...
My expected hash; which should group the name, then group the date and sum the value:
=> [
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>7, "Tue Jan 21, 2020"=>4, "Wed Jan 22, 2020"=>1}},
{:name=>"FR-GP04", :data=>{"Mon Jan 20, 2020"=>2, "Tue Jan 21, 2020"=>13, "Wed Jan 22, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Tue Jan 21, 2020"=>5, "Thu Jan 23, 2020"=>4}},
...
However, after running this code:
arr.group_by {|h| h[:name]}.map { |k,v| { name: k, data: v.map {|h| h[:data]}.reduce(&:merge)}}
this is the output:
=> [
{:name=>"RP-AP02", :data=>{"Mon Jan 20, 2020"=>2, "Tue Jan 21, 2020"=>1, "Wed Jan 22, 2020"=>1}},
{:name=>"RP-AP04", :data=>{"Mon Jan 20, 2020"=>2, "Tue Jan 21, 2020"=>4, "Wed Jan 22, 2020"=>3}},
{:name=>"RP-AP01", :data=>{"Tue Jan 21, 2020"=>5, "Thu Jan 23, 2020"=>3}},
...
The output generated does group the name and data, but does not sum the quantities. I'm grouping it by day here as an example, but would also like the option of grouping it by week & month. In the past 8 hours of monkeying with this, I've also tried using Groupdate to no avail.

There are many ways to obtain the desired return value. Here are two. First I define arr.
arr = [
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>5}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>1}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP02", :data=>{"Wed Jan 22, 2020"=>1}},
{:name=>"FR-GP04", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>4}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>6}},
{:name=>"FR-GP04", :data=>{"Wed Jan 22, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Tue Jan 21, 2020"=>5}},
{:name=>"FR-GP01", :data=>{"Thu Jan 23, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Thu Jan 23, 2020"=>1}}]
The first calculation employs the methods Enumerable#group_by and Hash#transform_values.
arr.group_by { |h| h[:name] }
.map do |k,v|
{ name: k,
data: v.group_by do |h|
h[:data].keys.first
end.transform_values { |a| a.sum { |h| h[:data].values.first }}
}
end
#=> [{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>7,
"Tue Jan 21, 2020"=>4,
"Wed Jan 22, 2020"=>1}},
{:name=>"FR-GP04", :data=>{"Mon Jan 20, 2020"=>2,
"Tue Jan 21, 2020"=>13,
"Wed Jan 22, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Tue Jan 21, 2020"=>5,
"Thu Jan 23, 2020"=>4}}]
Note:
arr.group_by { |h| h[:name] }
#=> {"FR-GP02"=>[{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>5}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>1}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP02", :data=>{"Wed Jan 22, 2020"=>1}}],
"FR-GP04"=>[{:name=>"FR-GP04", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>4}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP04", :data=>{"Tue Jan 21, 2020"=>6}},
{:name=>"FR-GP04", :data=>{"Wed Jan 22, 2020"=>3}}],
"FR-GP01"=>[{:name=>"FR-GP01", :data=>{"Tue Jan 21, 2020"=>5}},
{:name=>"FR-GP01", :data=>{"Thu Jan 23, 2020"=>3}},
{:name=>"FR-GP01", :data=>{"Thu Jan 23, 2020"=>1}}]}
map's block variables initially equal the following:
k = "FR-GP02"
v = [{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>2}},
{:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>5}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>1}},
{:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>3}},
{:name=>"FR-GP02", :data=>{"Wed Jan 22, 2020"=>1}}]
Then the value of :data in the first hash being created is computed as follows:
f = v.group_by do |h|
h[:data].keys.first
end
#=> {"Mon Jan 20, 2020"=>[
# {:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>2}},
# {:name=>"FR-GP02", :data=>{"Mon Jan 20, 2020"=>5}}],
# "Tue Jan 21, 2020"=>[
# {:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>1}},
# {:name=>"FR-GP02", :data=>{"Tue Jan 21, 2020"=>3}}],
# "Wed Jan 22, 2020"=>[
# {:name=>"FR-GP02", :data=>{"Wed Jan 22, 2020"=>1}}]}
and lastly,
f.transform_values { |a| a.sum { |h| h[:data].values.first }}
#=> {"Mon Jan 20, 2020"=>7, "Tue Jan 21, 2020"=>4, "Wed Jan 22, 2020"=>1}
Here is a second way to obtain the desired result.
arr.each_with_object(Hash.new(0)) do |g,h|
d, n = g[:data].flatten
h[[g[:name], d]] += n
end.group_by { |(name, _),_| name }
.map do |name,arr|
{ name: name, data: arr.each_with_object({}) { |((_,d),t),h| h[d] = t } }
end
#=> (as above)
The steps are as follows.
s = arr.each_with_object(Hash.new(0)) do |g,h|
d, n = g[:data].flatten
h[[g[:name], d]] += n
end
#=> {["FR-GP02", "Mon Jan 20, 2020"]=>7,
# ["FR-GP02", "Tue Jan 21, 2020"]=>4,
# ["FR-GP02", "Wed Jan 22, 2020"]=>1,
# ["FR-GP04", "Mon Jan 20, 2020"]=>2,
# ["FR-GP04", "Tue Jan 21, 2020"]=>13,
# ["FR-GP04", "Wed Jan 22, 2020"]=>3,
# ["FR-GP01", "Tue Jan 21, 2020"]=>5,
# ["FR-GP01", "Thu Jan 23, 2020"]=>4}
This uses the form of Hash::new that takes an argument called its default value (usually, as here, zero) and no block. If a hash is defined
h = Hash.new(0)
and--possibly after adding key-value pairs--does not have a key k, h[k] will return the default value. This means that in the expression
h[[g[:name], d]] += n
if h does not have a key [g[:name], d] the value of h for that key is initialized to zero before n is added. If h does have that key the current value of that key is increased by n.
Continuing the calculation,
t = s.group_by { |(name,_),_| name }
#=> {"FR-GP02"=>[[["FR-GP02", "Mon Jan 20, 2020"], 7],
# [["FR-GP02", "Tue Jan 21, 2020"], 4],
# [["FR-GP02", "Wed Jan 22, 2020"], 1]],
# "FR-GP04"=>[[["FR-GP04", "Mon Jan 20, 2020"], 2],
# [["FR-GP04", "Tue Jan 21, 2020"], 13],
# [["FR-GP04", "Wed Jan 22, 2020"], 3]],
# "FR-GP01"=>[[["FR-GP01", "Tue Jan 21, 2020"], 5],
# [["FR-GP01", "Thu Jan 23, 2020"], 4]]}
Lastly,
t.map do |name,arr|
{ name: name, data: arr.each_with_object({}) { |((_,d),t),h| h[d] = t } }
end
#=> (as above)
Here and earlier I've made good use of Ruby's powerful technique called Array decomposition. See also this article.

Related

Ruby - How to sum values of a hash based on a given date (=key)?

I have these values in a hash:
{nil=>0,
Thu, 03 Dec 2015=>#<BigDecimal:7ff496381db8,'0.151875E2',18(27)>,
Fri, 04 Dec 2015=>#<BigDecimal:7ff496381cf0,'0.214375E2',18(27)>,
Wed, 09 Dec 2015=>#<BigDecimal:7ff496381c28,'0.6229E2',18(27)>,
Thu, 10 Dec 2015=>#<BigDecimal:7ff496381b60,'0.1243E2',18(27)>,
Fri, 11 Dec 2015=>#<BigDecimal:7ff496381a98,'0.1243E2',18(27)>,
Mon, 14 Dec 2015=>#<BigDecimal:7ff4963819d0,'0.6611E2',18(27)>,
Tue, 15 Dec 2015=>#<BigDecimal:7ff496381908,'0.625E1',18(18)>,
Wed, 16 Dec 2015=>#<BigDecimal:7ff496381840,'0.73345E2',18(27)>,
Thu, 17 Dec 2015=>#<BigDecimal:7ff496381778,'0.31845E2',18(27)>,
Fri, 18 Dec 2015=>#<BigDecimal:7ff4963816b0,'0.409225E2',18(27)>,
Mon, 21 Dec 2015=>#<BigDecimal:7ff4963815e8,'0.8019E2',18(27)>,
Mon, 28 Dec 2015=>#<BigDecimal:7ff496381520,'0.3125E2',18(27)>,
Mon, 04 Jan 2016=>#<BigDecimal:7ff496381458,'0.125E2',18(27)>,
Wed, 06 Jan 2016=>#<BigDecimal:7ff496381390,'0.625E2',18(27)>,
Thu, 07 Jan 2016=>#<BigDecimal:7ff4963812c8,'0.9111E2',18(27)>,
Fri, 08 Jan 2016=>#<BigDecimal:7ff4963811d8,'0.11972E3',18(27)>,
Mon, 11 Jan 2016=>#<BigDecimal:7ff4963810e8,'0.5022E2',18(27)>,
Wed, 13 Jan 2016=>0, Thu, 14 Jan 2016=>0, Fri, 15 Jan 2016=>0,
Wed, 09 Mar 2016=>#<BigDecimal:7ff496380eb8,'0.258125E2',18(27)>,
Tue, 15 Mar 2016=>#<BigDecimal:7ff496380da0,'0.631825E2',18(27)>,
Wed, 16 Mar 2016=>#<BigDecimal:7ff496380cd8,'0.504225E2',18(27)>,
Thu, 17 Mar 2016=>#<BigDecimal:7ff496380c10,'0.125E2',18(27)>,
Fri, 18 Mar 2016=>#<BigDecimal:7ff496380b48,'0.631825E2',18(27)>,
Mon, 21 Mar 2016=>#<BigDecimal:7ff496380a80,'0.167925E2',18(27)>,
Tue, 22 Mar 2016=>0}
I am looping through some calendar data (#dates) and from here I get two variables containing a specific year and month:
#dates.each do |d|
current_yer = d.strftime('%Y') #2016
current_month = d.strftime('%m') # 01 - january
Now, I have variables containing the dates, I would like to print out a sum of all data in the hash; so in this case, I would like to get something like this as output:
2016 01: SUM of all January items in the hash
specifically, SUM of these:
Mon, 04 Jan 2016=>#<BigDecimal:7ff496381458,'0.125E2',18(27)>,
Wed, 06 Jan 2016=>#<BigDecimal:7ff496381390,'0.625E2',18(27)>,
Thu, 07 Jan 2016=>#<BigDecimal:7ff4963812c8,'0.9111E2',18(27)>,
Fri, 08 Jan 2016=>#<BigDecimal:7ff4963811d8,'0.11972E3',18(27)>,
Mon, 11 Jan 2016=>#<BigDecimal:7ff4963810e8,'0.5022E2',18(27)>,
Wed, 13 Jan 2016=>0, Thu, 14 Jan 2016=>0, Fri, 15 Jan 2016=>0,
How to do this?
Thank you in advance.
You can try with select and sum:
#dates.select{|d, _| d.strftime('%Y %m') == '2016 01'}.values.sum
Construct example hash
First let's construct a hash (h) that is similar to yours, but a bit smaller:
g = {
"Thu, 03 Dec 2015"=> 1,
"Fri, 11 Dec 2015"=> 2,
"Mon, 14 Dec 2015"=> 3,
"Tue, 15 Dec 2015"=> 4,
"Wed, 16 Dec 2015"=> 5,
"Fri, 18 Dec 2015"=> 6,
"Mon, 21 Dec 2015"=> 7,
"Mon, 04 Jan 2016"=> 8,
"Fri, 08 Jan 2016"=> 9,
"Wed, 13 Jan 2016"=> 0,
"Thu, 14 Jan 2016"=> 0,
"Wed, 09 Mar 2016"=>10,
"Tue, 15 Mar 2016"=>11,
"Wed, 16 Mar 2016"=>12,
"Mon, 21 Mar 2016"=>13,
"Tue, 22 Mar 2016"=> 0 }
require 'date'
require 'bigdecimal'
h = { nil=>0 }.tap { |h| g.each { |k,v|
h[Date.strptime(k, "%a, %d %b %Y")] = v.zero? ? 0 : BigDecimal.new(v) } }
#=> {nil=>0,
# #<Date: 2015-12-03 ((2457360j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef915c8e0,'0.1E1',9(27)>,
# #<Date: 2015-12-11 ((2457368j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914ff28,'0.2E1',9(27)>,
# #<Date: 2015-12-14 ((2457371j,0s,0n),+0s,2299161j) >=>
# #<BigDecimal:7faef914f938,'0.3E1',9(27)>,
# #<Date: 2015-12-15 ((2457372j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914f7a8,'0.4E1',9(27)>,
# #<Date: 2015-12-16 ((2457373j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914f320,'0.5E1',9(27)>,
# #<Date: 2015-12-18 ((2457375j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914e8d0,'0.6E1',9(27)>,
# #<Date: 2015-12-21 ((2457378j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914dde0,'0.7E1',9(27)>,
# #<Date: 2016-01-04 ((2457392j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914dca0,'0.8E1',9(27)>,
# #<Date: 2016-01-08 ((2457396j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914d390,'0.9E1',9(27)>,
# #<Date: 2016-01-13 ((2457401j,0s,0n),+0s,2299161j)> =>
# 0,
# #<Date: 2016-01-14 ((2457402j,0s,0n),+0s,2299161j)> =>
# 0,
# #<Date: 2016-03-09 ((2457457j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef914cd28,'0.1E2',9(27)>,
# #<Date: 2016-03-15 ((2457463j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef913ff60,'0.11E2',9(27)>,
# #<Date: 2016-03-16 ((2457464j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef913f8d0,'0.12E2',9(27)>,
# #<Date: 2016-03-21 ((2457469j,0s,0n),+0s,2299161j)> =>
# #<BigDecimal:7faef913f560,'0.13E2',9(27)>,
# #<Date: 2016-03-22 ((2457470j,0s,0n),+0s,2299161j)> =>
# 0
}
Here I've used the class methods Date::strptime and BigDecimal::new.
Sum BigDecimal values by month
We can now use Hash#reject, Enumerable#group_by, Enumerable#map, Enumerable#reduce (aka inject) and Array#to_h to obtain the results you require:
sums =
h.reject { |k,_| k.nil? }.
group_by { |k,_| [k.year, k.month] }.
map { |yr_and_mon, arr| [yr_and_mon, arr.reduce(0) { |t,(_,bd)| t+bd }] }.
to_h
#=> {[2015, 12]=>#<BigDecimal:7faef9197eb8,'0.28E2',9(18)>,
# [2016, 1]=>#<BigDecimal:7faef9197b70,'0.17E2',9(18)>,
# [2016, 3]=>#<BigDecimal:7faef9197760,'0.46E2',9(18)>}
We see that the BigDecimal values more easily by converting them to integers:
sums.merge(sums) { |*,v| v.to_i }
#=> {[2015, 12]=>28, [2016, 1]=>17, [2016, 3]=>46}
Compare these results with the hash g at the beginning of this answer.
I used the form of Hash#merge that employs a block to determine the values of keys that are present in both hashes being merged. As I am merging sums with itself, the block is used to determine the values of all keys.
Once you have the hash sums it's an easy task to print the totals by month and year in any format you want.
The steps
h1 = h.reject { |k,_| k.nil? }.group_by { |k,_| [k.year, k.month] }
#=> {[2015, 12]=>[[#<Date: 2015-12-03 ((2457360j,0s,0n),+0s,2299161j)>,
# #<BigDecimal:7faef915c8e0,'0.1E1',9(27)>],
# [#<Date: 2015-12-11 ((2457368j,0s,0n),+0s,2299161j)>,
# #<BigDecimal:7faef914ff28,'0.2E1',9(27)>],
...
# #<BigDecimal:7faef9093850,'0.46E2',9(18)>]]
a1 = h1.map { |yr_and_mon, arr| [yr_and_mon, arr.reduce(0) { |t,(_,bd)| t+bd }] }
#=> [[[2015, 12], #<BigDecimal:7faef9029dd8,'0.28E2',9(18)>],
# [[2016, 1], #<BigDecimal:7faef90296a8,'0.17E2',9(18)>],
# [[2016, 3], #<BigDecimal:7faef9028dc0,'0.46E2',9(18)>]]
a1.to_h
#=> {[2015, 12]=>#<BigDecimal:7faef9098710,'0.28E2',9(18)>,
# [2016, 1]=>#<BigDecimal:7faef9093da0,'0.17E2',9(18)>,
# [2016, 3]=>#<BigDecimal:7faef9093850,'0.46E2',9(18)>}

Merge hash with another hash

I have two hash like
h1 = {
DateTime.new(2015, 7, 1),in_time_zone => 0,
DateTime.new(2015, 7, 2).in_time_zone => 10,
DateTime.new(2015, 7, 4).in_time_zone => 20,
DateTime.new(2015, 7, 5).in_time_zone => 5
}
h2 = {
DateTime.new(2015, 7, 1).in_time_zone => 0,
DateTime.new(2015, 7, 2).in_time_zone => 0,
DateTime.new(2015, 7, 3).in_time_zone => 0
}
I want to merge h1 and h2, don't merge if key already exist, so that will result look like (datetime format with time zone shortened for readability)
result
#=> {
# Wed, 01 Jul 2015 01:00:00 EST +01:00 => 0,
# Thu, 02 Jul 2015 01:00:00 EST +01:00 => 10,
# Fri, 03 Jul 2015 01:00:00 EST +01:00 => 0,
# Sat, 04 Jul 2015 01:00:00 EST +01:00 => 20,
# Sun, 05 Jul 2015 01:00:00 EST +01:00 => 5
# }
I have tried with h1.merge(h2) and h2.merge(h1) but it can be put key and value of h2 to h1.
arr = []
h = h1.merge(h2)
h.each{|k, v| arr.include?(v) ? h.delete(k) : arr << v }
#=> {#<DateTime: 2015-07-01T00:00:00+00:00 ((2457205j,0s,0n),+0s,2299161j)>=>0,
#<DateTime: 2015-07-04T00:00:00+00:00 ((2457208j,0s,0n),+0s,2299161j)>=>20,
#<DateTime: 2015-07-05T00:00:00+00:00 ((2457209j,0s,0n),+0s,2299161j)>=>5}
You will have only three key-value pairs, not 5 as you expect, because hash in Ruby is collection of unique keys and their values.

Remove duplicates from Ruby arrays and sum

I have the following array of arrays [date, value]:
array = [[12 Mar 2015, 0], [12 Mar 2015, 5], [13 Mar 2015, 0], [14 Mar 2015, 49], [15 Mar 2015, 51], [15 Mar 2015, 10], [16 Mar 2015, 110], [17 Mar 2015, 0], [18 Mar 2015, 31], [19 Mar 2015, 47], [20 Mar 2015, 0], [21 Mar 2015, 0], [22 Mar 2015, 138], [22 Mar 2015, 10], [23 Mar 2015, 0]]
You can see that there are arrays with duplicate dates. How would one sum the values while grouping by the dates? This is what I am looking for:
array = [[12 Mar 2015, 5], [13 Mar 2015, 0], [14 Mar 2015, 49], [15 Mar 2015, 61], [16 Mar 2015, 110], [17 Mar 2015, 0], [18 Mar 2015, 31], [19 Mar 2015, 47], [20 Mar 2015, 0], [21 Mar 2015, 0], [22 Mar 2015, 148], [23 Mar 2015, 0]]
Your array of days should look like
array = [["12 Mar 2015", 0], ["12 Mar 2015", 5], ["13 Mar 2015", 0], ["14 Mar 2015", 49], ["15 Mar 2015", 51], ["15 Mar 2015", 10], ["16 Mar 2015", 110], ["17 Mar 2015", 0], ["18 Mar 2015", 31], ["19 Mar 2015", 47], ["20 Mar 2015", 0], ["21 Mar 2015", 0], ["22 Mar 2015", 138], ["22 Mar 2015", 10], ["23 Mar 2015", 0]]
grouped = array.inject(Hash.new(0)) do |result, itm|
result[itm.first] += itm.last
result
end.to_a
UPDATED
Many thanks to #nathanvda, inject({}) do |hash, [time, index]| was my mistake. In any case his solution is clearer.
array.inject({}) do |hash, item|
time, index = item.to_a
hash[time] = hash.fetch(time, 0) + index
hash
end.to_a

How to output a date range for each quarter of a year in Ruby?

Thanks to some people on this board I was able to come up with a function that returns a number of date ranges:
years = [2013, 2012, 2011, 2010, 2009]
def month_ranges
years.flat_map { |y|
12.downto(1).map { |m| Date.new(y,m,1)..Date.new(y,m,-1) }
}
end
# =>
[
01 Dec 2013..31 Dec 2013,
01 Nov 2013..31 Nov 2013,
01 Oct 2013..31 Oct 2013,
01 Sep 2013..31 Sep 2013,
01 Aug 2013..31 Aug 2013,
....
]
Now, is there a way to return the four quarters of a year as well?
So the output will be something like:
# =>
[
01 Oct 2013..31 Dec 2013,
01 Jul 2013..31 Sep 2013,
01 Apr 2013..31 Jun 2013,
01 Jan 2013..31 Mar 2013
]
(Note: If a month has 30 or 31 days doesn't really matter in this case.)
Thanks to anyone who can help.
This should work (based on month_ranges, i.e. last quarter comes first):
def quarter_ranges
years.flat_map { |y|
3.downto(0).map { |q|
Date.new(y, q * 3 + 1, 1)..Date.new(y, q * 3 + 3, -1)
}
}
end
Or a bit more verbose and maybe easier to understand:
def quarter_ranges
years.flat_map { |y|
[
Date.new(y, 10, 1)..Date.new(y, 12, -1),
Date.new(y, 7, 1)..Date.new(y, 9, -1),
Date.new(y, 4, 1)..Date.new(y, 6, -1),
Date.new(y, 1, 1)..Date.new(y, 3, -1)
]
}
end
You can use beginning_of_quarter and end_of_quarter to define quarters.
For example, if I want to group a date_range according to quarters I could do the following:
((Date.today - 1.year)..Date.today).group_by(&:beginning_of_quarter)
The keys in this case are the beginning of each quarter:
((Date.today - 1.year)..Date.today).group_by(&:beginning_of_quarter).keys
=> [Sun, 01 Jul 2012, Mon, 01 Oct 2012, Tue, 01 Jan 2013, Mon, 01 Apr 2013, Mon, 01 Jul 2013]
What about something like this:
> now = Time.now.beginning_of_month
=> 2013-09-01 00:00:00 +0200
> now..(now + 3.months)
=> 2013-09-01 00:00:00 +0200..2013-12-01 00:00:00 +0100
I'd do something like:
require 'date'
def quarters(y)
q = []
(1..4).each do |s|
q << (Date.new(y, s * 3 - 1, 1)..Date.new(y, s * 3, -1))
end
return q
end

How to identify if the given day, month, and year, combine a legal date?

Given 3 numbers: DD, MM, YYYY, what is the easiest way to know if they combine a legal date ?
Examples:
14, 05, 2011 => Legal
29, 02, 2011 => Illegal
29, 02, 2012 => Legal
35, 11, 1989 => Illegal
14, 18, 2011 => Illegal
14, 00, 2011 => Illegal
00, 11, 1979 => Illegal
31, 11, 1979 => Illegal
You can use valid_date? But it's YYYY, MM, DD:
irb(main):015:0> require 'date'
=> true
irb(main):021:0> Date::valid_date?(2011,05,14)
=> true
irb(main):022:0> Date::valid_date?(2011,02,29)
=> false
irb(main):023:0> Date::valid_date?(2012,02,29)
=> true
Date has a method valid_civil? .
require 'date'
dates = DATA.readlines.map{|line| line.split(', ').map(&:to_i)}
dates.each do |date|
d, m, y = date
puts Date.valid_civil?(y, m, d)
end
__END__
14, 05, 2011
29, 02, 2011
29, 02, 2012
35, 11, 1989
14, 18, 2011
14, 00, 2011
00, 11, 1979
31, 11, 1979
One option is to use something like:
require 'time'
def valid(year,month,day)
Time.parse "#{year}#{month}#{day}" rescue return false
return true
end

Resources