Rails: Calculate days between date range given in string format - ruby-on-rails

How could I calculate days from a given date range in Rails Controller.
Example:
daterange = "August 24 - Sept 11, 2016"
Desired output: 19
Also we have to keep in mind that the date range string may also change to something:
"December 24 2016 - Jan 11, 2017"

Try this:
(daterange.split("-")[1].to_date - daterange.split("-")[0].to_date).to_i + 1
# daterange = "August 24 - Sept 11, 2016"
# => 19
# daterange = "December 24 2016 - Jan 11, 2017"
# => 19

This should work:
def day_difference(daterange)
daterange.split('-').map(&:to_date).inject { |r,e| (e-r).to_i + 1 }
end
With your given examples:
day_difference("August 24 - Sept 11, 2016")
# => 19
day_difference("December 24 2016 - Jan 11, 2017")
# => 19

Related

Rails - Convert string to time

I'm trying to parse these string into time: "3 on Jun 20", "Jun 20 at 3", "Jun 20 at 300".
Using DateTime.parse didnt parse "3", "300" into "3:00 AM", it just returns Wed, 20 Jun 2018 00:00:00 +0000.
Anyone has any idea to parse these integer into time?
There's Chronic, a "natural language date/time parser":
require 'chronic'
Chronic.parse('3 on Jun 20') #=> 2018-06-20 15:00:00 +0200
Chronic.parse('Jun 20 at 3') #=> 2018-06-20 15:00:00 +0200
Chronic.parse('Jun 20 at 300') #=> 2018-06-20 15:00:00 +0200
Just out of curiosity, trying to reinvent chronic in 4 LOCs :)
["3 on Jun 20", "Jun 20 at 3", "Jun 20 at 300"].map do |dt|
d, t = dt.split(/\s+at\s+/i)
t, d = dt.split(/\s+on\s+/i) unless t
return [dt] unless t && d
t = t[0..-3] + (t[-2..-1] ? ":" << t[-2..-1] : t[/.{,2}\z/] + ":00")
[d, t] # [["Jun 20", "3:00"], ["Jun 20", "3:00"], ["Jun 20", "3:00"]]
end.map { |dt| DateTime.parse dt.join ' ' }
Use strptime to parse a custom format:
DateTime.strptime("3 on Jun 20", "%H on %b %d")
https://ruby-doc.org/stdlib-2.5.0/libdoc/date/rdoc/DateTime.html#method-c-strptime

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.

Generating a list of weeks and months given a date-range

Given a date-range, how can I generate a sequential list of
weeks
months
that are included in the date range? For example, if the date-range is Jan 15 - Apr 29, then for the weeks it should be
15 Jan - 17, 18 - 24, 25 - 31 ... 19 Apr - 25, 26 - 29 Apr
where Jan 18th is Sunday, so in this case the week starts on Sunday, but it can also be Monday, doesn't matter. And for the months:
15 Jan - 31, 1 Feb - 28, 1 - 31 March, 1 - 29 Apr
What's the easiest way to do it?
Not sure if this is the easiest way, but it is a way. (Lazy way?)
Months: You start with a range like:
range = (Date.today..6.months.from_now)
Then you can get each month like:
months = range.to_a.map(&:beginning_of_month).uniq
#lookup docs on strftime to get exactly what you want here
months.map { |date| date.strftime('%Y %b') }
For weeks, you can start with months:
weeks = months.flat_map { |date|
m_weeks = [date]
until (week = date + 1.week) && week.month > date.month
m_weeks << week
end
m_weeks
}
#enter some string in strftime (im too lazy to look it up now)
weeks.map { |date| date.strftime('') }
I didn't test this, but I think it should work. Anyway, there are many ways to do this.
Ok so to get all weeks in the month by weeks Monday - Sunday, you can do something like this:
m_days = (Date.today.beginning_of_month..Date.today.end_of_month).to_a
day_offset = m_days.first.wday - 1
day_offset.times { m_days.unshift(nil) }
Then if you want to get an array of strings that have the first day of the week to the last day of the week you could do this.
weeks = []
m.days.each_slice(7) do |w_days|
w_days.compact!
weeks << "#{w_days.first} - #{w_days.last}"
end
You have two problems:
determining the beginning and end of each week between the start date and end date.
formatting the dates for display.
I have addressed the first problem only, but have provided all the information required to format the result in any way desired.
We are given:
start_date = "Jan 15, 2015"
end_date = "Apr 29, 2015"
We then compute the array weeks as follows:
require 'date'
days = (Date.parse(start_date)..Date.parse(end_date)).to_a
weeks = ([days.shift(7-days.first.wday)].concat(
days.each_slice(7).to_a)).map { |w|
[w.first, w.last].map { |d| [d.year, d.month, d.day, d.wday] } }
# => [[[2015, 1, 15, 4], [2015, 1, 17, 6]],
# [[2015, 1, 18, 0], [2015, 1, 24, 6]],
# [[2015, 1, 25, 0], [2015, 1, 31, 6]],
# [[2015, 2, 1, 0], [2015, 2, 7, 6]],
# ...
# [[2015, 4, 19, 0], [2015, 4, 25, 6]],
# [[2015, 4, 26, 0], [2015, 4, 29, 3]]]
Each element of weeks corresponds to a week and contains two arrays, one for the first day of the week (a Sunday, for all weeks after the first); the second for the last day of the week (a Saturday for all weeks other than the last). The arrays for individual days contain the year, month (1-12), day of month and day of week (Sunday: 0, Monday: 1,...Saturday: 6).
You can then use the Date class constants (search for "constants" at Date) to format the results.

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

Resources