Working with Times and Dates - THE RUBY WAY, Third Edition (2015)

THE RUBY WAY, Third Edition (2015)

Chapter 7. Working with Times and Dates

Does anybody really know what time it is?

—Chicago, Chicago IV

One of the most complex and confusing areas of human life is that of measuring time. To come to a complete understanding of the subject, you would need to study physics, astronomy, history, law, business, and religion. Astronomers know (as most of us don’t!) that solar time and sidereal time are not quite the same thing, and why a “leap second” is occasionally added to the end of the year. Historians know that the calendar skipped several days in October 1582, when Italy converted from the Julian calendar to the Gregorian. Few people know the difference between astronomical Easter and ecclesiastical Easter (which are almost always the same). Many people don’t know that century years not divisible by 400 (such as the year 1900) are not leap years.

Performing calculations with times and dates is common in computing but has traditionally been somewhat tedious in most programming languages. It is tedious in Ruby, too, because of the nature of the data. However, Ruby has taken some incremental steps toward making these operations easier.

As a courtesy to the reader, we’ll go over a few terms that may not be familiar to everyone. Most of these come from standard usage or from other programming languages.

Greenwich Mean Time (GMT) is an old term not really in official use anymore. The new global standard is Coordinated Universal Time (or UTC, from the French version of the name). GMT and UTC are virtually the same thing; over a period of years, the difference will be on the order of seconds. Much of the software in the industry does not distinguish between the two at all (nor does Ruby).

Daylight Saving Time is a semiannual shift in the official time, amounting to a difference of one hour. Thus, the U.S., time zones change every year from ending in ST (Standard Time) to DT (Daylight Time) and back again. This annoying trick is used in most (though not all) of the United States and in many other countries.

The epoch is a term borrowed from UNIX lore. In this realm, a time is typically stored internally as a number of seconds from a specific point in time (called the epoch). In UNIX, the epoch was midnight January 1, 1970, GMT. (Note that in U.S. time zones, this will actually be the preceding December 31.) An epoch time is one measured by the distance in time from that point.

The Time class is used for most operations. The Date and DateTime classes provide some extra flexibility. Let’s look at some common uses of these.

7.1 Determining the Current Time

The most fundamental problem in time/date manipulation is to answer the question: “What are the time and date right now?” In Ruby, when we create a Time object with no parameters, it is set to the current date and time:

t0 = Time.new

Time.now is a synonym:

t0 = Time.now

Note that the resolution of system clocks varies from one architecture to another. It often includes microseconds, and two Time objects created immediately in succession will almost always record different times.

7.2 Working with Specific Times (Post-Epoch)

Most software only needs to work with dates in the future or in the recent past. For these circumstances, the Time class is adequate. The relevant class methods are mktime, local, gm, and utc.

The mktime method creates a new Time object based on the parameters passed to it. These time units are given in reverse, from longest to shortest: year, month, day, hours, minutes, seconds, microseconds. All but the year are optional; they default to the lowest possible value. The microseconds may be ignored on many architectures. The hours must be between 0 and 23 (that is, a 24-hour clock).

t1 = Time.mktime(2014) # January 1, 2014 at 0:00:00
t2 = Time.mktime(2014,3)
t3 = Time.mktime(2014,3,15)
t4 = Time.mktime(2014,3,15,21)
t5 = Time.mktime(2014,3,15,21,30)
t6 = Time.mktime(2014,3,15,21,30,15) # March 15, 2014 9:30:15 pm

Note that mktime assumes the local time zone. In fact, Time.local is a synonym for it:

t7 = Time.local(2014,3,15,21,30,15) # March 15, 2014 9:30:15 pm

The Time.gm method is basically the same, except that it assumes GMT (or UTC). Because the authors are in the U.S. Pacific time zone, we would see an eight-hour difference here:

t8 = Time.gm(2014,3,15,21,30,15) # March 15, 2014 9:30:15 pm
# This is only 2:30:15 pm in Pacific time!

The Time.utc method is a synonym:

t9 = Time.utc(2014,3,15,21,30,15) # March 15, 2014 9:30:15 pm
# Again, 2:30:15 pm Pacific time.

There is one more important item to note. All these methods can take an alternate set of parameters. The instance method to_a (which converts a time to an array of relevant values) returns a set of values in this order: seconds, minutes, hours, day, month, year, day of week (0..6), day of year (1..366), daylight saving (true or false), and time zone (as a string).

Thus, these are also valid calls:

t0 = Time.local(0,15,3,20,11,1979,2,324,false,"GMT-7:00")
t1 = Time.gm(*Time.now.to_a)

However, in the first example, do not fall into the trap of thinking that you can change the computable parameters such as the day of the week (in this case, 2 meaning Tuesday). A change like this simply contradicts the way our calendar works, and it will have no effect on the time object created. November 20, 1979 was a Tuesday regardless of how we might write our code.

Finally, note that there are obviously many ways to attempt coding incorrect times, such as a 13th month or a 35th day of the month. In cases like these, an ArgumentError will be raised.

7.3 Determining the Day of the Week

There are several ways to determine the day of the week. First, the instance method to_a returns an array of time information. You can access the seventh element, which is a number from 0 to 6 (0 meaning Sunday and 6 meaning Saturday).

time = Time.now
day = time.to_a[6] # 2 (meaning Tuesday)

It’s better to use the instance method wday, as shown here:

day = time.wday # 2 (meaning Tuesday)

But both these techniques are a little cumbersome. Sometimes we want the value coded as a number, but more often we don’t. To get the actual name of the weekday as a string, we can use the strftime method. This name will be familiar to C programmers. There are nearly two dozen different specifiers that it recognizes, enabling us to format dates and times more or less as we want (see Section 7.21, “Formatting and Printing Time Values”).

name = time.strftime("%A") # "Tuesday"

It’s also possible to obtain an abbreviated name:

tln = time.strftime("%a") # "Tue"

7.4 Determining the Date of Easter

Traditionally, this holiday is one of the most difficult to compute because it is tied to the lunar cycle. The lunar month does not go evenly into the solar year, and thus anything based on the moon can be expected to vary from year to year.

The algorithm we present here is a well-known one that has made the rounds. We have seen it coded in BASIC, Pascal, and C. We now present it to you in Ruby:

def easter(year)
c = year/100
n = year - 19*(year/19)
k = (c-17)/25
i = c - c/4 - (c-k)/3 + 19*n + 15
i = i - 30*(i/30)
i = i - (i/28)*(1 -(i/28)*(29/(i+1))*((21-n)/11))
j = year + year/4 + i + 2 - c + c/4
j = j - 7*(j/7)
l = i - j
month = 3 + (l+40)/44
day = l + 28 - 31*(month/4)
[month, day]
end

date = easter 2014 # Find month/day for 2001
t = Time.local 2014, *date # Pass parameters to Time.local
puts t # 2014-04-20 00:00:00 -0700

One reader on seeing this section on Easter asked, “Ecclesiastical or astronomical?” Truthfully, I don’t know. If you find out, let us all know.

I’d love to explain this algorithm to you, but I don’t understand it myself. Some things must be taken on faith, and in the case of Easter, this may be especially appropriate.

7.5 Finding the Nth Weekday in a Month

Sometimes for a given month and year, we want to find the date of the third Monday in the month, or the second Tuesday, and so on. The code in Listing 7.1 makes that calculation simple.

If we are looking for the nth occurrence of a certain weekday, we pass n as the first parameter. The second parameter is the number of that weekday (0 meaning Sunday, 1 meaning Monday, and so on). The third and fourth parameters are the month and year, respectively.

Listing 7.1 Finding the Nth Weekday


def nth_wday(n, wday, month, year)
if (!n.between? 1,5) or
(!wday.between? 0,6) or
(!month.between? 1,12)
raise ArgumentError, "Invalid day or date"
end
t = Time.local year, month, 1
first = t.wday
if first == wday
fwd = 1
elsif first < wday
fwd = wday - first + 1
elsif first > wday
fwd = (wday+7) - first + 1
end
target = fwd + (n-1)*7
begin
t2 = Time.local year, month, target
rescue ArgumentError
return nil
end
if t2.mday == target
t2
else
nil
end
end


The peculiar-looking code near the end of the method is put there to counteract a long-standing tradition in the underlying time-handling routines. You might expect that trying to create a date of November 31 would result in an error of some kind. You would be mistaken. Most systems would happily (and silently) convert this to December 1. If you are an old-time UNIX hacker, you may think this is a feature; otherwise, you may consider it a bug.

We will not venture an opinion here as to what the underlying library code ought to do or whether Ruby ought to change that behavior. But we don’t want to have this routine perpetuate the tradition. If you are looking for the date of, say, the fifth Friday in November 2014, you will get anil value back (rather than December 5, 2014).

7.6 Converting Between Seconds and Larger Units

Sometimes we want to take a number of seconds and convert to days, hours, minutes, and seconds. This following routine will do just that:

def sec2dhms(secs)
time = secs.round # Get rid of microseconds
sec = time % 60 # Extract seconds
time /= 60 # Get rid of seconds
mins = time % 60 # Extract minutes
time /= 60 # Get rid of minutes
hrs = time % 24 # Extract hours
time /= 24 # Get rid of hours
days = time # Days (final remainder)
[days, hrs, mins, sec] # Return array [d,h,m,s]
end

t = sec2dhms(1000000) # A million seconds is...

puts "#{t[0]} days," # 11 days,
puts "#{t[1]} hours," # 13 hours,
puts "#{t[2]} minutes," # 46 minutes,
puts " and #{t[3]} seconds." # and 40 seconds.

We could, of course, go up to higher units. However, a week is not an overly useful unit, a month is not a well-defined term, and a year is far from being an integral number of days.

We also present here the inverse of that function:

def dhms2sec(days,hrs=0,min=0,sec=0)
days*86400 + hrs*3600 + min*60 + sec
end

7.7 Converting to and from the Epoch

For various reasons, we may want to convert back and forth between the internal (or traditional) measure and the standard date form. Internally, dates are stored as a number of seconds since the epoch.

The Time.at class method creates a new time given the number of seconds since the epoch:

epoch = Time.at(0) # Find the epoch (1 Jan 1970 GMT)
newmil = Time.at(978307200) # Happy New Millennium! (1 Jan 2001)

The inverse is the instance method to_i which converts to an integer:

now = Time.now # 2014-07-23 17:24:26 -0700
sec = now.to_i # 1406161466

If you need microseconds, you can use to_f to convert to a floating point number.

7.8 Working with Leap Seconds: Don’t!

Ah, but my calculations, people say,
Reduced the year to better reckoning? Nay,
’Twas only striking from the calendar
Unborn Tomorrow and dead Yesterday.

—Omar Khayyam, The Rubaiyat (trans. Fitzgerald)

You want to work with leap seconds? Our advice: Don’t.

Leap seconds are very real. One was added to the year 2012, when June 30’s final minute had 61 seconds rather than the usual 60. Although the library routines have for years allowed for the possibility of a 61-second minute, our experience has been that most systems do not keep track of leap seconds. When we say “most,” we mean all the ones we’ve ever checked.

t0 = Time.gm(2012, 6, 30, 23, 59, 59)
puts t0 + 1 # 2012-07-01 00:00:00 UTC

It is (barely) conceivable that Ruby could add a layer of intelligence to correct for this. At the time of this writing, however, there are no plans to add such functionality.

7.9 Finding the Day of the Year

The day number within the year is sometimes called the Julian date; this is not directly related to the Julian calendar, which has gone out of style. Other people insist that this usage is not correct, so we won’t use it from here on.

No matter what you call it, there will be times you want to know what day of the year it is, from 1 to 366. This is easy in Ruby; we use the yday method:

t = Time.now
day = t.yday # 315

7.10 Validating a Date or Time

As we saw in Section 7.5Finding the Nth Weekday in a Month,” the standard date/time functions do not check the validity of a date, but “roll it over” as needed. For example, November 31 is translated to December 1.

At times, this may be the behavior you want. If it is not, you will be happy to know that the standard library Date does not regard such a date as valid. We can use this fact to perform validation of a date as we instantiate it:

class Time

def Time.validate(year, month=1, day=1,
hour=0, min=0, sec=0, usec=0)
require "date"

begin
d = Date.new(year,month,day)
rescue
return nil
end
Time.local(year,month,day,hour,min,sec,usec)
end

end

t1 = Time.validate(2014,11,30) # Instantiates a valid object
t2 = Time.validate(2014,11,31) # Returns nil

Here, we have taken the lazy way out; we simply set the return value to nil if the parameters passed in do not form a valid date (as determined by the Date class). We have made this method a class method of Time by analogy with the other methods that instantiate objects.

Note that the Date class can work with dates prior to the epoch. This means that passing in a date such as 31 May 1961 will succeed as far as the Date class is concerned. But when these values are passed into the Time class, an ArgumentError will result. We don’t attempt to catch that exception here because we think it’s appropriate to let it be caught at the same level as (for example) Time.local, in the user code.

Speaking of Time.local, we used that method here; but if we wanted GMT instead, we could have called the gmt method. It would be a good idea to implement both flavors.

7.11 Finding the Week of the Year

The definition of “week number” is not absolute and fixed. Various businesses, coalitions, government agencies, and standards bodies have differing concepts of what it means. This stems, of course, from the fact that the year can start on any day of the week; we may or may not want to count partial weeks, and we may start on Sunday or Monday.

We offer only three alternatives in this section. The first two are made available by the Time method strftime. The %U specifier numbers the weeks starting from Sunday, and the %W specifier starts with Monday.

The third possibility comes from the Date class. It has an accessor called cweek, which returns the week number based on the ISO 8601 definition (which says that week 1 is the week containing the first Thursday of the year).

If none of these three suits you, you may have to “roll your own.” We present these three in a single code fragment:

require "date"

# Let's look at May 1 in the years
# 2005 and 2015.

t1 = Time.local(2005,5,1)
d1 = Date.new(2005,5,1)

week1a = t1.strftime("%U").to_i # 18
week1b = t1.strftime("%W").to_i # 17
week1c = d1.cweek # 17

t2 = Time.local(2015,5,1)
d2 = Date.new(2015,5,1)

week2a = t2.strftime("%U").to_i # 17
week2b = t2.strftime("%W").to_i # 17
week2c = d2.cweek # 18

7.12 Detecting Leap Years

The Date class has two class methods: julian_leap? and gregorian_leap?. Only the latter is of use in recent years. It also has a method called leap?, which is an alias for the gregorian_leap? method.

require "date"
flag1 = Date.julian_leap? 1700 # true
flag2 = Date.gregorian_leap? 1700 # false
flag3 = Date.leap? 1700 # false

Every child knows the first rule for leap years: The year number must be divisible by four. Fewer people know the second rule, that the year number must not be divisible by 100; and fewer still know the exception, that the year can be divisible by 400. In other words, a century year is a leap year only if it is divisible by 400; therefore, 1900 was not a leap year, but 2000 was. (This adjustment is necessary because a year is not exactly 365.25 days, but a little less, approximately 365.2422 days.)

The Time class does not have a method like this, but if we needed one, it would be simple to create:

class Time

def Time.leap? year
if year % 400 == 0
true
elsif year % 100 == 0
false
elsif year % 4 == 0
true
else
false
end

end

I’ve written this to make the algorithm explicit; an easier implementation, of course, would be simply to call the Date.leap? method from this one. I implement this as a class method by analogy with the Date class methods. It could also be implemented as an instance method.

7.13 Obtaining the Time Zone

The accessor zone in the Time class returns a String representation of the time zone name:

z1 = Time.gm(2000,11,10,22,5,0).zone # "UTC"
z2 = Time.local(2000,11,10,22,5,0).zone # "PST"

Unfortunately, times are stored relative to the current time zone, not the one with which the object was created. If necessary, you can do a little arithmetic here.

7.14 Working with Hours and Minutes Only

We may want to work with times of day as strings. Once again, strftime comes to our aid.

We can print the time with hours, minutes, and seconds if we want:

t = Time.now
puts t.strftime("%H:%M:%S") # 22:07:45

We can print hours and minutes only (and, using the trick of adding 30 seconds to the time, we can even round to the nearest minute):

puts t.strftime("%H:%M") # 22:07
puts (t+30).strftime("%H:%M") # 22:08

Finally, if we don’t like the standard 24-hour (or military) clock, we can switch to the 12-hour clock. It’s appropriate to add a meridian indicator then (AM/PM):

puts t.strftime("%I:%M %p") # 10:07 PM

There are other possibilities, of course. Use your imagination.

7.15 Comparing Time Values

The Time class conveniently mixes in the Comparable module so that dates and times may be compared in a straightforward way:

t0 = Time.local(2014,11,10,22,15) # 10 Nov 2014 at 22:15
t1 = Time.local(2014,11,9,23,45) # 9 Nov 2014 at 23:45
t2 = Time.local(2014,11,12,8,10) # 12 Nov 2014 at 8:10
t3 = Time.local(2014,11,11,10,25) # 11 Nov 2014 at 10:25

if t0 > t1 then puts "t0 > t1" end
if t1 != t2 then puts "t1 != t2" end
if t1 <= t2 then puts "t1 <= t2" end
if t3.between?(t1,t2)
puts "t3 is between t1 and t2"
end

# All four if statements test true

7.16 Adding Intervals to Time Values

We can obtain a new time by adding an interval to a specified time; the number is interpreted as a number of seconds:

t0 = Time.now
t1 = t0 + 60 # Exactly one minute past t0
t2 = t0 + 3600 # Exactly one hour past t0
t3 = t0 + 86400 # Exactly one day past t0

The function dhms2sec (defined in Section 7.6, “Converting Between Seconds and Larger Units,” earlier in the chapter) might be helpful here (recall that the hours, minutes, and seconds all default to 0):

t4 = t0 + dhms2sec(5,10) # Ahead 5 days, 10 hours
t5 = t0 + dhms2sec(22,18,15) # Ahead 22 days, 18 hrs, 15 min
t6 = t0 - dhms2sec(7) # Exactly one week ago

Don’t forget that we can move backward in time by subtracting, as seen with t6 in the preceding code example.

7.17 Computing the Difference in Two Time Values

We can find the interval of time between two points in time. Subtracting one Time object from another gives us a number of seconds:

today = Time.local(2014,11,10)
yesterday = Time.local(2014,11,9)
diff = today - yesterday # 86400 seconds

Once again, the function sec2dhms comes in handy (this is defined in Section 7.6, “Converting Between Seconds and Larger Units”):

past = Time.local(2012,9,13,4,15)
now = Time.local(2014,11,10,22,42)
diff = now - past # 68153220.0
unit = sec2dhms(diff)
puts "#{unit[0]} days," # 788 days,
puts "#{unit[1]} hours," # 19 hours,
puts "#{unit[2]} minutes," # 27 minutes,
puts "and #{unit[3]} seconds." # and 0 seconds.

7.18 Working with Specific Dates (Pre-Epoch)

The standard library Date provides a class of the same name for working with dates that precede midnight GMT, January 1, 1970.

Although there is some overlap in functionality with the Time class, there are significant differences. Most notably, the Date class does not handle the time of day at all. Its resolution is a single day. Also, the Date class performs more rigorous error-checking than the Time class; if you attempt to refer to a date such as June 31 (or even February 29 in a non-leap year), you will get an error. The code is smart enough to know the different cutoff dates for Italy and England switching to the Gregorian calendar (in 1582 and 1752, respectively), and it can detect “nonexistent” dates that are a result of this switchover. This standard library is a tangle of interesting and arcane code. We do not have space to document it further here.

7.19 Time, Date, and DateTime

Ruby has three basic classes dealing with dates and times: Time, Date, and DateTime.

• The Time class is mostly a wrapper for the underlying time functions in the C library. These are typically based on the UNIX epoch and therefore cannot represent times before 1970.

• The Date class was created to address this shortcoming of the Time class. It can easily deal with older dates, such as Leonardo da Vinci’s birthday (April 15, 1452), and it is intelligent about the dates of calendar reform. But it has its own shortcoming; for example, it can’t deal with the time of day that Leonardo was born. It deals strictly with dates.

• The DateTime class inherits from Date and tries to be the best of both worlds. It can represent dates as well as Date can, and times as well as Time can. This is often the “right” way to represent a date-time value. The tradeoff made is one of speed, and DateTime can be vastly slower if used extensively for calculations.

But don’t be fooled into thinking that a DateTime is just a Date with an embedded Time. There are, in fact, Time methods missing from DateTime, such as nsec, dst?, utc, and others.

Both the Date and DateTime classes are part of the standard library, and must be loaded with require "date" before they can be used. Once the Date library is loaded, conversions are provided by methods with the highly predictable to_time, to_date, and to_datetimemethods.

7.20 Parsing a Date or Time String

A date and time can be formatted as a string in many different ways because of abbreviations, varying punctuation, different orderings, and so on. Because of the various ways of formatting, writing code to decipher such a character string can be daunting. Consider these examples:

s1 = "9/13/14 2:15am"
s2 = "1961-05-31"
s3 = "11 July 1924"
s4 = "April 17, 1929"
s5 = "20 July 1969 16:17 EDT" # That's one small step...
s6 = "Mon Nov 13 2000"
s7 = "August 24, 79" # Destruction of Pompeii
s8 = "8/24/79"

The Time and Date libraries provide minimal parsing via the parse class method. Unfortunately, those methods assume that two-digit years are in a recent century, thus misplacing the destruction of Pompeii by 1900 years. They also cannot interpret American-style dates, such as those in the preceding s1 and s8 examples.

To solve the century issue, use the second argument to Date.parse. When it is set to false, the century will no longer be guessed:

Date.parse("August 24, 79", false) # 0079-08-24

Keep in mind that the default is to guess the century, which may or may not be the one you intend. Two-digit years up to 69 are interpreted as 20XX, whereas years 70 and above are rendered 19XX.

To parse American-style dates, and in fact a huge range of more exotic formats, use the Chronic gem (installed with gem install chronic). Although it shares the Time limits on two-digit years, it provides fairly exhaustive time parsing.

Chronic.parse "next tuesday"
Chronic.parse "3 weeks ago monday at 5pm"
Chronic.parse "time to go home" # Well, not every possible time

If Chronic can’t understand a time, it will simply return nil instead of raising an exception.

When parsing times, be careful of time zones. CST, for example, has at least five different meanings around the world. See Section 7.22, “Time Zone Conversions,” for more information about how to handle times in multiple time zones.

7.21 Formatting and Printing Time Values

You can obtain the canonical representation of the date and time by calling the to_s method. This is the same as the result you would get if doing a simple puts of a time value.

Similarly, a traditional UNIX format that includes the day of the week is available via the asctime method (“ASCII time”); it has an alias called ctime, for those who already know it by that name.

The strftime method of class Time formats a date and time in almost any form you can think of. Other examples in this chapter have shown the use of the directives %a, %A, %U, %W, %H, %M, %S, %I, and %p; we list here all the remaining directives that strftime recognizes:

%b Abbreviated month name ("Jan")
%B Full month name ("January")
%c Preferred local date/time representation
%d Day of the month (1..31)
%j Day of the year (1..366); so-called "Julian date"
%m Month as a number (1..12)
%w Day of the week as a number (0..6)
%x Preferred representation for date (no time)
%y Two-digit year (no century)
%Y Four-digit year
%Z Time zone name
%% A literal "%" character

For more information, consult a Ruby reference.

7.22 Time Zone Conversions

In plain Ruby, it is only possible to work with two time zones: UTC (or GMT) is one, and the other is whatever time zone you happen to be in.

The utc method converts a time to UTC (changing the receiver in place). There is also an alias named gmtime.

You might expect that it would be possible to convert a time to an array, tweak the time zone, and convert it back. The trouble with this is that all the class methods that create a Time object, such as local and gm (or their aliases mktime and utc), use either your local time zone or UTC.

There is a workaround to get time zone conversions. This does require that you know the time difference in advance. See the following code fragment:

mississippi = Time.local(2014,11,13,9,35) # 9:35 am CST
california = mississippi - 2*3600 # Minus two hours

time1 = mississippi.strftime("%X CST") # 09:35:00 CST
time2 = california.strftime("%X PST") # 07:35:00 PST

The %X directive to strftime that we see here simply uses the hh:mm:ss format as shown.

This kind of conversion is not usable around the world, however, because some zone abbreviations are used by multiple countries in different zones.

If you will be converting between time zones frequently, or simply need to accept and manipulate times that include many time zones, the ActiveSupport gem supplies extensions to Time that add thorough time zone support.

require 'active_support/time'
Time.zone = -8
Time.zone.name # "Pacific Time (US & Canada)"
Time.zone.now # Wed, 25 Jun 2014 12:20:35 PDT -07:00
Time.zone.now.in_time_zone("Hawaii") # 09:20:36 HST -10:00

For more information on how to use ActiveSupport to handle the full range of worldwide time zones, see the gem documentation online, especially the TimeWithZone class.

7.23 Determining the Number of Days in a Month

Although there is no built-in function to do this, it is provided by the days_in_month method added to Time by the ActiveSupport gem. Without ActiveSupport, you can easily write a simple method for this:

require 'date'
def month_days(month,year=Date.today.year)
mdays = [nil,31,28,31,30,31,30,31,31,30,31,30,31]
mdays[2] = 29 if Date.leap?(year)
mdays[month]
end

days = month_days(5) # 31 (May)
days = month_days(2,2000) # 29 (February 2000)
days = month_days(2,2100) # 28 (February 2100)

7.24 Dividing a Month into Weeks

Imagine that you wanted to divide a month into weeks—for example, to print a calendar. The following code does that. The array returned is made up of subarrays, each of size seven (7); Sunday corresponds to the first element of each inner array. Leading entries for the first week and trailing entries for the last week may be nil:

def calendar(month,year)
days = month_days(month,year)
t = Time.mktime(year,month,1)
first = t.wday
list = *1..days
weeks = [[]]
week1 = 7 - first
week1.times { weeks[0] << list.shift }
nweeks = list.size/7 + 1
nweeks.times do |i|
weeks[i+1] ||= []
7.times do
break if list.empty?
weeks[i+1] << list.shift
end
end
pad_first = 7-weeks[0].size
pad_first.times { weeks[0].unshift(nil) }
pad_last = 7-weeks[0].size
pad_last.times { weeks[-1].unshift(nil) }
weeks
end

arr = calendar(12,2008) # [[nil, 1, 2, 3, 4, 5, 6],
# [7, 8, 9, 10, 11, 12, 13],
# [14, 15, 16, 17, 18, 19, 20],
# [21, 22, 23, 24, 25, 26, 27],
# [28, 29, 30, 31, nil, nil, nil]]

To illustrate it a little better, the following method prints out this array of arrays:

def print_calendar(month,year)
weeks = calendar(month,year)
weeks.each do |wk|
wk.each do |d|
item = d.nil? ? " "*4 : " %2d " % d
print item
end
puts
end
puts
end

# Output:
# 1 2 3 4 5 6
# 7 8 9 10 11 12 13
# 14 15 16 17 18 19 20
# 21 22 23 24 25 26 27
# 28 29 30 31

7.25 Conclusion

In this chapter, we have looked at how the Time class works as a wrapper for the underlying C-based functions. We’ve seen its features and its limitations.

We’ve seen the motivation for the Date and DateTime classes and the functionality they provide. We’ve looked at how to convert between these three kinds of objects, and we’ve added a few useful methods of our own.

That ends our discussion of times and dates. Let’s move on to look at arrays, hashes, and other enumerable data structures in Ruby.