Nine Interesting Tidbits About Dates In Ruby
Note: This article sat in my drafts folder for a long time. I recently had to look up how to determine the weekday-ness of a date, so I decided to finally give this a publish.
1. To use Date objects in Ruby, you must require the ‘date’ library.
# ruby-2.5.0
> Date.new
NameError (uninitialized constant Date
Did you mean? Data)
> require 'date'
=> true
> Date.new
=> #<Date: -4712-01-01 ((0j,0s,0n),+0s,2299161j)>
Unsurprisingly, Date is already included in Rails.
2. Date.new
takes arguments in the order year, month, day — the same order as ISO 8601. This format is also sometimes referred to as Big Endian.
# ruby-2.5.0
> Date.new(2018, 3, 19)
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
You can use -1
to refer to the last day of a month.
#ruby-2.5.0
> Date.new(2018, 3, -1)
=> #<Date: 2018-03-31 ((2458209j,0s,0n),+0s,2299161j)>
There are also some shortcut methods, like Date.today
. However, you’ll need to include ActiveSupport if you want access to Date.yesterday
or Date.tomorrow
.
# ruby-2.5.0
> today = Date.today
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
> Date.tomorrow
NoMethodError (undefined method `tomorrow' for Date:Class)
3. Attempting to create a date that does not exist will raise an exception.
# ruby-2.5.0
> Date.new(2018, 2, 31)
ArgumentError (invalid date)
If concerned about possible invalid date parameters, you can first check them using Date.valid_date?
.
# ruby-2.5.0
> Date.valid_date?(2018, 2, 31)
=> false
Date.valid_date?(2018, 2, 28)
=> true
4. You can access the various parts of the date with methods like .year
, .month
, and .day
.
# ruby-2.5.0
> today.year
=> 2018
> today.month
=> 3
> today.day
=> 19
5. There are boolean methods to determine the day of the week.
# ruby-2.5.0
> today.monday?
=> true
> today.friday?
=> false
You can use .wday
to get a zero-based number for the day of the week (with 0 being Sunday and 6 being Saturday). Alternatively, .cwday
yields a one-based number (with 1 being Monday and 7 being Sunday).
# ruby-2.5.0
> sunday = Date.new(2018, 3, 18)
=> #<Date: 2018-03-18 ((2458196j,0s,0n),+0s,2299161j)>
> sunday.wday
=> 0
> sunday.cwday
=> 7
6. Date objects respond to the upto
or downto
messages, so you can iterate over a date range.
# ruby-2.5.0
> sunday.upto(today) { |date| puts date }
2018-03-18
2018-03-19
7. A Date can be created from a string using parse
. Impressively, it is compatible with a variety of formats.
# ruby-2.5.0
> Date.parse('2018-03-19')
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
> Date.parse('20180319')
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
> Date.parse('19th Mar 2018')
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
> Date.parse('Monday March 19, 2018')
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
If you want to be more explicit about your date format, use strptime
and pass a date format string. The default format is ‘yyyy-m-dd.’
# ruby-2.5.0
> Date.strptime('2018-03-19')
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
> Date.strptime('2018/03/19')
ArgumentError (invalid date)
> Date.strptime('March 19, 2018', '%B %d, %Y')
=> #<Date: 2018-03-19 ((2458197j,0s,0n),+0s,2299161j)>
8. strftime
reverses this operation, returning a string representation of a date (I always read this as string from time). The same formatting rules apply. By default, to_s
formats the date in the same ISO 8601 format we’ve come to expect, as does iso8601
.
# ruby-2.5.0
> today.to_s
=> "2018-03-19"
> today.iso8601
=> "2018-03-19"
> today.strftime('%Y-%m-%d')
=> "2018-03-19"
> today.strftime('%B %d, %Y')
=> "March 19, 2018"
However, for the special case of converting a date to a weekday, consider using DAYNAMES
. The constant DAYNAMES
is an array of the names of the days of the week, and, I would argue, more semantic than strftime
in this case.
# ruby-2.5.0
> Date::DAYNAMES
=> ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
> Date::DAYNAMES[today.wday]
=> "Monday"
> today.strftime('%A')
=> "Monday"
Performance wise, DAYNAMES
appears to be a clear winner.
# ruby-2.5.0
> require 'benchmark'
=> true
> puts Benchmark.measure { 10000.times { Date::DAYNAMES[today.wday] } }
0.001394 0.000007 0.001401 ( 0.001395)
> puts Benchmark.measure { 10000.times { today.strftime('%A') } }
0.003813 0.000030 0.003843 ( 0.003839)
(You can learn more about the Benchmark module here.)
9. Basic arithmetic can be used with date objects, though next
/succ
and prev
are more semantic ways to accomplish the same goal.
# ruby-2.5.0
> today + 1
=> #<Date: 2018-03-20 ((2458198j,0s,0n),+0s,2299161j)>
> today.next
=> #<Date: 2018-03-20 ((2458198j,0s,0n),+0s,2299161j)>
next_day
and prev_day
can take a number of days as an argument. Without the argument, they work the same as next
and prev
.
# ruby-2.5.0
> today + 5
=> #<Date: 2018-03-24 ((2458202j,0s,0n),+0s,2299161j)>
> today.next_day(5)
=> #<Date: 2018-03-24 ((2458202j,0s,0n),+0s,2299161j)>
Need to go further? Try next_month
or next_year
.
#ruby-2.5.0
> today.next_month(5)
=> #<Date: 2018-08-19 ((2458350j,0s,0n),+0s,2299161j)>
> today.next_year(5)
=> #<Date: 2023-03-19 ((2460023j,0s,0n),+0s,2299161j)>
Finally, the <<
and >>
operators can be used to backtrack or advance a given number of months.
# ruby-2.5.0
> today >> 5
=> #<Date: 2018-08-19 ((2458350j,0s,0n),+0s,2299161j)>
Further Reading
Check out Rob Dodson’s great article about Date, Chronic, and Active Support.