Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get month names between two dates

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?

like image 481
Sebastien Avatar asked Sep 19 '12 20:09

Sebastien


2 Answers

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.

like image 177
the Tin Man Avatar answered Oct 04 '22 00:10

the Tin Man


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"]
like image 40
MrTheWalrus Avatar answered Oct 03 '22 22:10

MrTheWalrus