I have a hash whose keys are strings expressing dates in format "%W %Y":
{"11 2016"=>255000.0, "12 2016"=>255000.0, "13 2016"=>255000.0, "14 2016"=>255000.0, "15 2016"=>255000.0, "16 2016"=>255000.0, "17 2016"=>255000.0, "18 2016"=>255000.0, "19 2016"=>255000.0, "20 2016"=>255000.0}
I want to convert the keys into Month/Year format to get something like this:
{"March 2016"=>255000.0, "March 2016"=>255000.0, "April 2016"=>255000.0, "April 2016"=>255000.0, "April 2016"=>255000.0, "May 2016"=>255000.0, "May 2016"=>255000.0, "May 2016"=>255000.0, "June 2016"=>255000.0, "June 2016"=>255000.0}
I tried this:
UPDATE
def conv_to_month(hash)
dates_array = []
values = []
hash.each do |k, v|
date = Date.strptime(k, '%W %Y')
newdate = date.strftime('%B %Y')
puts newdate
dates_array << newdate
values << v
end
result = Hash[dates_array.zip(values)]
end
but I get an invalid date error, and I do not understand why.
You can parse the date strings via strptime:
date = Date.strptime('11 2016', '%W %Y')
#=> #<Date: 2016-03-14>
and re-format the resulting date via strftime:
date.strftime('%B %Y')
#=> "March 2016"
But keep in mind that a hash can only contain each key once, so you can't have two keys with "March 2016" (unless you enable compare_by_identity). You might want to use an array instead or – alternatively – use the given hash and format the string using a helper when printing the string (formatting is usually view-related).
One simple way would be something like this:
data = {"11 2016"=>255000.0, "12 2016"=>255000.0, "13 2016"=>255000.0, "14 2016"=>255000.0, "15 2016"=>255000.0, "16 2016"=>255000.0, "17 2016"=>255000.0, "18 2016"=>255000.0, "19 2016"=>255000.0, "20 2016"=>255000.0}
data.map {|k, v| month, year = k.split(' '); [[ Date::MONTHNAMES[month.to_i], year].join(' '), v]}.to_h
But in this case the first part of the date must correspond to month number correctly. Like 1 would be January and 12 would be December.
I have two Date objects, for example:
first = Fri, 02 Dec 2016
last = Wed, 01 Mar 2017
What is the most efficient way to get a unique array of months and years between them? In this case I'm after:
Dec 2016
Jan 2017
Feb 2017
Mar 2017
require 'date'
def doit(first, last)
first = first << 1
(12*last.year + last.month - 12*first.year - first.month + 1).
times.map { |i| (first = first >> 1).strftime("%b %Y") }
end
first = Date.parse('Fri, 02 Dec 2016')
last = Date.parse('Wed, 01 Mar 2017')
doit(first, last)
#=> ["Dec 2016", "Jan 2017", "Feb 2017", "Mar 2017"]
Note that
(12*last.year + last.month - 12*first.year - first.month + 1)
equals the number of months covered by the range.
You could create an array of dates, then use strftime to set the correct format, and uniq to avoid repeated values, like this:
(first..last).map{ |date| date.strftime("%b %Y") }.uniq
#=> ["Dec 2016", "Jan 2017", "Feb 2017", "Mar 2017"]
Since user asks for most efficient way (and just for fun) here's a simple benchmark of the proposed solutions:
require 'benchmark'
Benchmark.bmbm(10) do |bm|
bm.report('Cary') do
first = Date.new(1000, 1, 1)
last = Date.new(2100, 1, 1)
def doit(first, last)
(12*last.year + last.month - 12*first.year - first.month).times.map do
first.strftime("%b %Y")
first = first >> 1
end
end
doit(first, last)
end
bm.report('Simple Lime') do
first = Date.new(1000, 1, 1)
last = Date.new(2100, 1, 1)
dates = []
while first.beginning_of_month < last.end_of_month
dates << first.strftime("%b %Y")
first = first.next_month
end
end
bm.report('Máté') do
first = Date.new(1000, 1, 1)
last = Date.new(2100, 1, 1)
(first.beginning_of_month..last).map { |d| d.strftime("%b %Y") if d.day == 1 }.compact
end
bm.report('Gerry/Dan') do
first = Date.new(1000, 1, 1)
last = Date.new(2100, 1, 1)
(first..last).map{ |date| date.strftime("%b %Y") }.uniq
end
end
Results:
Rehearsal -----------------------------------------------
Cary 0.020000 0.000000 0.020000 ( 0.025968)
Simple Lime 0.190000 0.000000 0.190000 ( 0.192860)
Máté 0.460000 0.020000 0.480000 ( 0.481839)
Gerry/Dan 0.810000 0.020000 0.830000 ( 0.835931)
-------------------------------------- total: 1.520000sec
user system total real
Cary 0.020000 0.000000 0.020000 ( 0.024871)
Simple Lime 0.150000 0.000000 0.150000 ( 0.150696)
Máté 0.390000 0.010000 0.400000 ( 0.398637)
Gerry/Dan 0.710000 0.010000 0.720000 ( 0.711155)
Not a pretty one-liner, but also doesn't walk through each day individually so should be a fair bit faster for large ranges.
first = Date.new(2016, 12, 2)
last = Date.new(2017, 3, 1)
dates = []
while first.beginning_of_month < last.end_of_month
dates << first.strftime("%b %Y")
first = first.next_month
end
puts dates.inspect
# => ["Dec 2016", "Jan 2017", "Feb 2017", "Mar 2017"]
Assuming these variables:
first = Date.new(2016, 12, 2)
last = Date.new(2017, 3, 1)
A one-liner solution:
(first.beginning_of_month..last).map { |d| d.strftime("%b %Y") if d.day == 1 }.compact
#=> ["Dec 2016", "Jan 2017", "Feb 2017", "Mar 2017"]
start_date=Date.new(2016,12,2)
end_date=Date.new(2017,3,1)
(start_date..end_date).map{ |d| d.strftime("%b %Y") }.uniq
=> ["Dec 2016", "Jan 2017", "Feb 2017", "Mar 2017"]
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)>}
I have an array bed_times with Time instances in UTC format:
bed_times = [
Time.utc(2015, 12, 10, 5, 58, 24),
Time.utc(2015, 12, 9, 3, 35, 28),
Time.utc(2015, 12, 8, 6, 32, 26),
Time.utc(2015, 12, 7, 1, 43, 28),
Time.utc(2015, 12, 5, 7, 49, 30),
Time.utc(2015, 12, 04, 7, 2, 30)
]
#=> [2015-12-10 05:58:24 UTC,
# 2015-12-09 03:35:28 UTC,
# 2015-12-08 06:32:26 UTC,
# 2015-12-07 01:43:28 UTC,
# 2015-12-05 07:49:30 UTC,
# 2015-12-04 07:02:30 UTC]
I am trying to get the average bedtime, but I'm not getting the correct result
ave = Time.at(bed_times.map(&:to_f).inject(:+) / bed_times.size)
result is
2015-12-07 01:26:57 -0800
which is not correct. Also, I want to then convert the average time to a different time zone
I tried
Time.zone = 'Pacific Time (US & Canada)'
Time.zone.parse(ave.to_s)
2015-12-07 01:26:57 -0800
This is not correct either.
You have to calculate the average on the gap from midnight.
A not elegant (but fast) solution could be:
# Keep only time
bed_times.map! { |bt| Time.parse(bt.split(" ")[1]) }
# calculate the gap from 00:00:00
gap_from_midnight = bed_times.map do |bt|
if bt > Time.parse("12:00:00")
gap = (bt.to_f - Time.parse("24:00:00").to_f)
else
gap = (bt.to_f - Time.parse("00:00:00").to_f)
end
gap.to_i
end
# average in sec
avg_in_sec = gap_from_midnight.inject(:+) / bed_times.size
# average in UTC time zone
avg = Time.at(avg_in_sec).utc # => 1970-01-01 05:26:57 UTC (result for bed_times array)
# average in PST time zone (see note)
avg_pst = Time.parse(avg.to_s).in_time_zone("Pacific Time (US & Canada)") # => Wed, 31 Dec 1969 21:26:57 PST -08:00 (result for bed_times array)
# Keep only time
avg_pst.strftime("%H:%M:%S") # => "21:26:57" (result for bed_times array)
With your bed_times array (with the values as a string)
bed_times = [
"2015-12-10 05:58:24 UTC",
"2015-12-09 03:35:28 UTC",
"2015-12-08 06:32:26 UTC",
"2015-12-07 01:43:28 UTC",
"2015-12-05 07:49:30 UTC",
"2015-12-04 07:02:30 UTC"
]
the average is :
05:26:57 in UTC zone
21:26:57 in PST zone
With another array like this
bed_times = [
"2015-12-10 01:00:00 UTC",
"2015-12-09 23:00:00 UTC",
"2015-10-19 18:00:00 UTC",
]
the average is:
22:00:00 in UTC zone
14:00:00 in PST zone
note: .in_time_zone is a helper from ActiveSupport::TimeWithZone
http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
As I understand, you are given an array arr that contains the UTC times of bedtimes in areas that are on PST time. You wish to compute the average bedtime.
Code
def avg_bedtime(arr)
avg = Time.at(arr.reduce(0) do |t,s|
lt = Time.parse(s).localtime("-08:00")
t + Time.new(2000, 1, (lt.hour < 12) ? 2 : 1, lt.hour, lt.min, lt.sec, "-08:00").to_f
end/arr.size)
"%d:%d:%d" % [avg.hour, avg.min, avg.sec]
end
Example
arr = ["2015-12-10 08:58:24 UTC", "2015-12-09 03:35:28 UTC",
"2015-12-08 06:32:26 UTC", "2015-12-07 01:43:28 UTC",
"2015-12-05 07:49:30 UTC", "2015-12-04 07:02:30 UTC"]
I've changed the first time in this array to make it more interesting.
avg_bedtime(arr)
#=> "21:56:57"
Explanation
Let's begin by converting these strings to time objects:
utc = arr.map { |s| Time.parse(s) }
#=> [2015-12-10 08:58:24 UTC, 2015-12-09 03:35:28 UTC, 2015-12-08 06:32:26 UTC,
# 2015-12-07 01:43:28 UTC, 2015-12-05 07:49:30 UTC, 2015-12-04 07:02:30 UTC]
Recalling that PST is 8 hours later than UTC, we can use Time.localtime to convert to PST:
pst = utc.map { |t| t.localtime("-08:00") }
#=> [2015-12-10 00:58:24 -0800, 2015-12-08 19:35:28 -0800,
# 2015-12-07 22:32:26 -0800, 2015-12-06 17:43:28 -0800,
# 2015-12-04 23:49:30 -0800, 2015-12-03 23:02:30 -0800]
I will refer to bedtimes before noon to be "late" bedtimes and those later in the day to be "early" bedtimes. (This is of course arbitrary. If, for example, some of the individuals are shift workers, this could be a problem.) As you see, the first element of pst is a late bedtime and all others are early bedtimes.
I will now convert these time objects to time objects having the same time of day but a different date. Early bedtime objects will be assigned an arbitrary date (say, January 1, 2000) and late bedtime objects will be one day later (January 2, 2000):
adj = pst.map { |t| Time.new(2000, 1, (t.hour < 12) ? 2 : 1, t.hour, t.min, t.sec, "-08:00") }
#=> [2000-01-02 00:58:24 -0800, 2000-01-01 19:35:28 -0800, 2000-01-01 22:32:26 -0800,
# 2000-01-01 17:43:28 -0800, 2000-01-01 23:49:30 -0800, 2000-01-01 23:02:30 -0800]
We can now convert the time objects to seconds since the epoch:
secs = adj.map { |t| t.to_f }
#=> [946803504.0, 946784128.0, 946794746.0, 946777408.0, 946799370.0, 946796550.0]
compute the average:
avg = secs.reduce(:+)/arr.size
#=> 946792617.6666666
convert back to a time object for the PST zone:
tavg = Time.at(avg)
#=> 2000-01-01 21:56:57 -0800
and, lastly, extract the time of day:
"%d:%d:%d" % [tavg.hour, tavg.min, tavg.sec]
# "21:56:57