I have to get all months names in order between two dates using Ruby. For example, I want to get:
['jan','feb','mars']
When I make a diff between:
1st january and March 15th
I also want it to work when the date diff is greater than one year.
Any idea?
I'd go with:
d1 = Date.parse('jan 1 2011')
d2 = Date.parse('dec 31 2012')
(d1..d2).map{ |m| m.strftime('%Y%m') }.uniq.map{ |m| Date::ABBR_MONTHNAMES[ Date.strptime(m, '%Y%m').mon ] }
=> ["Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"]
or:
(d1..d2).map{ |m| m.strftime('%Y%m') }.uniq.map{ |m| Date::ABBR_MONTHNAMES[ m[/\d\d$/ ].to_i ] }
which is probably a little faster.
The problem is the year boundary. You have to track years and months, not just the months, otherwise you'll remove all the duplicated month indexes when using uniq
to remove the days. I went with the YYYYMM
format, to get the right granularity.
require 'benchmark'
require 'date'
d1 = Date.parse('jan 1 2011')
d2 = Date.parse('dec 31 2012')
n = 100
Benchmark.bm(8) do |x|
x.report('strptime') { n.times { (d1..d2).map{ |m| m.strftime('%Y%m') }.uniq.map{ |m| Date::ABBR_MONTHNAMES[ Date.strptime(m, '%Y%m').mon ] } } }
x.report('regex') { n.times { (d1..d2).map{ |m| m.strftime('%Y%m') }.uniq.map{ |m| Date::ABBR_MONTHNAMES[ m[/\\d\\d$/ ].to_i ] } } }
end
user system total real
strptime 3.060000 0.020000 3.080000 ( 3.076614)
regex 2.820000 0.010000 2.830000 ( 2.829366)
EDIT:
Let's make it even more interesting.
I had some code smell that kept bugging me. I didn't like using Date.strftime
and Date.strptime
, so I took another run at the problem: Here are two more solutions that are running a lot faster, along with the benchmarks:
require 'benchmark'
require 'date'
def regex_months_between(d1, d2)
d1, d2 = [d1, d2].map{ |d| Date.parse(d) }.minmax
(d1..d2).map{ |m| m.strftime('%Y%m') }.uniq.map{ |m| Date::ABBR_MONTHNAMES[ m[/\d\d$/ ].to_i ] }
end
def months_between1(d1, d2)
d1, d2 = [d1, d2].map{ |d| Date.parse(d) }.minmax
months = (d2.mon - d1.mon) + (d2.year - d1.year) * 12
month_names = []
months.times{ |m|
month_names << Date::ABBR_MONTHNAMES[(d1 >> m).mon]
}
month_names << Date::ABBR_MONTHNAMES[d2.mon]
month_names
end
def months_between2(d1, d2)
d1, d2 = [d1, d2].map{ |d| Date.parse(d) }.minmax
months = (d2.mon - d1.mon) + (d2.year - d1.year) * 12
(d1.mon ... (d1.mon + months)).each_with_object(Date::ABBR_MONTHNAMES[d1.mon, 1]) { |month_offset, month_names_array|
month_names_array << Date::ABBR_MONTHNAMES[(d1 >> month_offset).mon]
}
end
puts regex_months_between('jan 1 2011', 'dec 31 2012').join(', ')
puts months_between1('jan 1 2011', 'dec 31 2012').join(', ')
puts months_between2('jan 1 2011', 'dec 31 2012').join(', ')
n = 100
Benchmark.bm(3) do |b|
b.report('rmb') { n.times { regex_months_between('jan 1 2011', 'dec 31 2012') } }
b.report('mb1') { n.times { months_between1('jan 1 2011', 'dec 31 2012') } }
b.report('mb2') { n.times { months_between2('jan 1 2011', 'dec 31 2012') } }
end
With output looking like:
Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
user system total real
rmb 2.810000 0.010000 2.820000 ( 2.820732)
mb1 0.060000 0.000000 0.060000 ( 0.057763)
mb2 0.060000 0.000000 0.060000 ( 0.057112)
Interesting. "rmb
" is now running way behind. Pulling it from the tests and bumping up the loops 100x:
n = 10_000
Benchmark.bm(3) do |b|
b.report('mb1') { n.times { months_between1('jan 1 2011', 'dec 31 2012') } }
b.report('mb2') { n.times { months_between2('jan 1 2011', 'dec 31 2012') } }
end
Which gives:
user system total real
mb1 5.570000 0.060000 5.630000 ( 5.615789)
mb2 5.570000 0.040000 5.610000 ( 5.611323)
It's basically a tie between the two new ways of getting the months. Being anal, I'd go with mb2
because it'd be a little bit faster if I was doing this millions of times, but your mileage might vary.
So, let's say you have two dates, d1
and d2
:
> d1 = 2.years.ago
=> Sun, 19 Sep 2010 15:51:18 CDT -05:00
> d2 = 1.month.ago
=> Sun, 19 Aug 2012 15:51:25 CDT -05:00
(In this case, they're ActiveSupport::TimeWithZone objects, but this works just as well with DateTime or what-have-you)
Make an array to contain your output:
m = []
Then fill it up with month abbreviations (using .strftime
) while incrementing the month:
while d1 <= d2.at_end_of_month
m << d1.strftime('%b')
d1 = d1 + 1.month
end
And there it is:
> m
=> ["Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With