Michael Victor
Michael Victor

Reputation: 891

Convert an array

I have a method that returns the year and month between a set of dates.

d = (from..to).map {|d| [ d.year, d.month ] }.uniq

I can iterate over each element like so:

d.each do |elm|
  #For Year
  puts elm[0]

  #For Month Number
  puts elm[1]
end

How can I change the method so that I can iterate as follows to enhance the readability of the code and in general make it easier as well?

elm.month_number
elm.year

Upvotes: 0

Views: 67

Answers (5)

Jordan Running
Jordan Running

Reputation: 106027

There's some great discussion in the other answers, but it seems silly to iterate over all 365 days in each year just to get 12 date objects when it's not at all necessary.

Instead of iterating from from to to, calculate an integer number of months from each and then iterate over that range. Take a look:

from = Date.civil(2013, 7, 1)
to = Date.civil(2015, 11, 23)

from_mos = 12 * from.year + from.month - 1
to_mos = 12 * to.year + to.month - 1

dates = (from_mos..to_mos).map do |mos|
  year, month = mos.divmod(12)
  Date.civil(year, month + 1, 1)
end

Now dates is an Enumerable yielding a Date for the first day of each month, i.e. 2013-07-01, 2013-08-01, … 2015-11-01, which you can use as below;

dates.each do |date|
  puts "Year #{date.year}, month #{date.month}"
end
# => Year 2013, month 7
#    Year 2013, month 8
#    ...
#    Year 2015, month 10
#    Year 2015, month 11

P.S. If you want to go to the trouble of defining a class or Struct, I recommend making it work directly with the Range API:

YearMonth = Struct.new(:year, :month) do
  def succ
    next_year, next_month = months.succ.divmod(12)
    self.class.new(next_year, next_month + 1)
  end

  def <=>(other)
    months <=> other.months
  end

  protected
  def months
    12 * year + month - 1
  end
end

Now you can just do this:

from = YearMonth.new(2013, 7)
to = YearMonth.new(2015, 11)

(from..to).each do |ym|
  puts "Year #{ym.year}, month #{ym.month}"
end

Upvotes: 0

sawa
sawa

Reputation: 168081

What you are doing is redundant. Keep the date elements as is (I assume they are Date objects).

d = (from..to).to_a.uniq_by{|elm| [elm.year, elm.month]}

class Date
  alias month_number month
end

d.each do |elm|
  elm.year
  ...
  elm.month_number
  ...
end

Upvotes: 1

K M Rakibul Islam
K M Rakibul Islam

Reputation: 34318

You can do something like this too (create a CustomDate class with year and month attributes):

class CustomDate
  attr_accessor :year, :month

  def initialize(year, month)
    self.year = year
    self.month = month
  end
end

(from..to).map { |d| CustomDate.new(d.year, d.month) }.uniq.each do |e|
  puts "year: #{e.year}"
  puts "month: #{e.month}"
end

Upvotes: 0

alexanderbird
alexanderbird

Reputation: 4198

I think you're looking for array auto-unpacking as described here: How to iterate over an array of arrays

d = (from..to).map {|d| [ d.year, d.month ] }.uniq

d.each do |year, month|
  # For Year
  puts year

  #For Month Number
  puts month
end

Upvotes: 2

Amadan
Amadan

Reputation: 198314

Use a class or a structure. e.g.

YearMonth = Struct.new(:year, :month)
d = (from..to).map{|d| YearMonth.new(d.year, d.month)}.uniq

although, this is not an optimal way of doing it. Consider this:

d = (from.year .. to.year).flat_map { |year|
  from_month = year == from.year ? from.month : 1
  to_month = year == to.year ? to.month : 12
  (from_month..to_month).map { |month| YearMonth.new(year, month) }
}

This avoids creating a huge array of dates, and another array that results from a map, and goes straight for months (even if it is not as compact).

Upvotes: 1

Related Questions