Date, Time, DateTime in Ruby and Rails
There are 3 different classes in Ruby that handle date and time. Date and DateTime classes are both from date library. And Time class from its own time library.
Both DateTime and Time classes can be used to handle year, month, day, hour, min, sec attributes. But underneath, Time class stores integer numbers, which presents the seconds intervals since the Epoch. We also call it unix time.
Time class has some limits. Firstly, it can only represent dates between 1970 and 2038 ( since Ruby v1.9.2, it can represent date from 1823-11-12 to 2116-02-20 ). Secondly, the time zone is limited to UTC and the system’s local time zone in ENV['TZ'].
Rails provide a really good time class called ActiveSupport::TimeWithZone. It contains all the features the Time class have, plus many improvements, such as the support for configurable time zones.
One thing worth to know is that, Rails will always convert time zone to UTC before it writes to or reads from the database, no matter what time zone you set in the configuration file. You can use <attribute_name>_before_type_cast
to get the original time that store in database. For example to get the original time zone of created_at
before the typecasting, you can do:
object.created_at_before_type_cast
In the following sections, I will show you some examples and useful snippet code for each class.
1. Time
# Get current time using the time zone of current local system
Time.now
# Get current time using the time zone of UTC
Time.now.utc
# Get the unix timestamp of current time => 1364046539
Time.now.to_i
# Convert from unix timestamp back to time form
Time.at(1364046539)
# Format the string output with #strftime method => "March 23, 2013 at 09:48 AM"
Time.at(1364046539).strftime("%B %e, %Y at %I:%M %p")
Youe can find more about formating the time and the details about format directives at Ruby’s Time document.
When using the Time class, I prefer to always convert it to unix timestamp, because the integer form can be easily stored, indexed or sorted. Also it can be used in the situation where the distance between two time is more important than the exact time itself, such use case can be found in tweets, where it shows 1 minute ago
instead of the actually time.
2. Time with Zone (ActiveSupport::TimeWithZone)
TimeWithZone instances implement the same API as Ruby Time instances, so that both instances are interchangeable.
Below are some examples that show you how to use TimeWithZone in Rails:
# Set the time zone for the TimeWithZone instance
Time.zone = 'Central Time (US & Canada)'
# Get current time using the time zone you set
Time.zone.now
# Convert from unix timestamp integer to a time instance using the time zone you set
Time.zone.at(1364046539)
# - Convert from unix timestamp back to a time instance
# - Set the time zone to Eastern Time
# - Change the output string format => "03/23/13 09:48 AM"
Time.at(1364046539).in_time_zone("Eastern Time (US & Canada)").strftime("%m/%d/%y %I:%M %p")
Rails provides a lot of very useful helper methods, using pretty straightforward English speaking format.
# Get the time of n day, week, month, year ago
1.day.ago
2.days.ago
1.week.ago
3.months.ago
1.year.ago
# Get the beginning of or end of the day, week, month ...
Time.now.beginning_of_day
30.days.ago.end_of_day
1.week.ago.end_of_month
# Convert time to unix timestamp
1.week.ago.beginning_of_day.to_i
# Convert time instance to date instance
1.month.ago.to_date
You can find more methods by checking the doc.
Time distance
Rails also provides time distance methods in ActionView::Helpers for you to get the twitter styled time format.
# inside of your .erb view files or under your help class
diff = Time.now.to_i - 1.hour.ago.to_i
distance_of_time_in_words(diff)
distance_of_time_in_words_to_now(1.hour.ago)
Customized time zone by user
For Rails application, you can set the default time zone under /config/application.rb
# /config/application.rb
config.time_zone = 'Central Time (US & Canada)'
To get a list of time zone names supported by Rails, you can use
ActiveSupport::TimeZone.zones_map(&:name)
Normally, we would like to provide a select input in a form for user to choose their desired time zone. You can provide an input field (e.g. use name :time_zone
) in a form like this:
<%= f.time_zone_select :time_zone %>
You can also add more options, e.g. only allow US time zone, and use Pacific Time Zone as default, as shown below:
# use US time zone only, with default
<%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)" %>
To apply user’s time zone setting, we will use the method called #use_zone, which override the Time.zone locally inside the supplied block. We can add an around_filter
inside of ApplicationController as suggested by railscast to make sure the time zone is set correctly for each request:
# /app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_filter :user_time_zone, if: :current_user
# ... ...
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
def user_time_zone(&block)
Time.use_zone(current_user.time_zone, &block)
end
end
3. Date and DateTime
For most cases, the Time with the time zone class from Rails’ ActiveSupport is sufficient. But sometimes, when you just need a string format of year, month and day, Date class still worth a try.
Remember to add require 'date'
when you encounter undefined method error, when trying to use Date class.
Both Date and DateTime come with #parse method, which allows you to convert a date string into a Date object. There is also a decent method called #strptime, which allows you to explicitly define the template format of the date and time you are going to parse. Examples are shown below:
require 'date'
# use parse to create a date object
Date.parse("2014-1-1")
DateTime.parse("2014-1-1")
# explicitly define the date format using strptime
Date.strptime("12/13/2013", "%m/%d/%Y")
You can also use date range to generate a list of date string. For example, in one of my applications, we use date string as the key to store count information in Redis:
# Generate date string in 30 days range
days_str = (30.days.ago.to_date...Date.today).map{ |date| date.strftime("%Y:%m:%d") }
Date also comes with a very handy array of day names, that can be easily used in your drop-down select field:
Date.today.wday # the day of week
Date::DAYNAMES[Date.today.wday] # => "Saturday"
Date::DAYNAMES.each_with_index.to_a # => [["Sunday", 0], ["Monday", 1], ["Tuesday", 2], ["Wednesday", 3], ["Thursday", 4], ["Friday", 5], ["Saturday", 6]]
# Use day names in select field:
# <%= select(:report, :day, Date::DAYNAMES.each_with_index.to_a, {:selected => 1} %>
Time, Date, DateTime are all interchangeable by using to_time, to_date, and to_datetime methods.
# Convert DateTime to Time
DateTime.parse('March 3rd 2013 04:05:06 AM').to_time.class # => Time
# Convert Time to Date
1.day.ago.to_date.class # => Date
# Convert to DateTime
Time.now.to_datetime
Furthermore, you can use #to_i method to change a Date or DateTime object into an Unix Timestamp which represents an integer number of seconds since the Epoch:
# Parse date string and convert to timestamp in seconds
Date.parse("2014-1-1").to_time.to_i
DateTime.parse("2014-1-1").to_time.to_i