which is an abstrac1on of dates and 1mes. Time is stored as the number of seconds (with frac1on) since the Unix Epoch which is January 1, 1970 00:00 UTC. Tuesday, April 3, 12
a frac1on part. Be aware of this when comparing two 1mes. They could appear to be equal, but are not. t1, t2 = Time.now, Time.now #=> [2012-04-03 19:00:00 -0500, 2012-04-03 19:00:00 -0500] t1 == t2 #=> false t1.usec #=> 594792 t2.usec #=> 594859 Tuesday, April 3, 12
creates a 1me object based on a passed 1me or number of seconds in local 1me. • Time.utc (or Time.gm) creates a UTC 1me based on a passed year, month, day, etc. • Time.local (or Time.mktime) creates a local 1me like utc does. • Time.new In Ruby 1.9 also takes year, month, day, etc. (Ruby 1.8 doesn’t have params) – Time.now is just Time.new without any parameters Tuesday, April 3, 12
only really deals with local (server 1me) and UTC 1me. However.... • With Ruby 1.9 you can call Time.new with utc_offset to get the specific 1me in any 1mezone. Ruby 1.8 doesn’t have this. Time.new(year, month, day, hour, min, sec, utc_offset) # utc_offset like “-06:00” Ruby 1.9‘s utc, local and new are supposed to have a secret handshake which can give you back a 1me in any 1me zone Time.utc(sec, min, hour, day, month, year, wday, yday, isdst, tz) but the wday, yday, isdst and tz parameters seem to get ignored as they don’t do anything. Tuesday, April 3, 12
Comparison with <=>, eql? • Simple conversion of utc -‐> local, local -‐> utc • .to_s, .strftime (more on these later, but we won’t delve into str[ime’s format strings) • .to_a, .to_i, .to_f • Pluck methods like .month, .hour, .wday, .yday, etc. • .usec for just the microseconds • .zone returns the 1me zone like “UTC” or “CDT” Tuesday, April 3, 12
gives us the Date, Time and DateTime classes we typically work with. There’s a li_le but of 1me zone stuff here but only enough to make us work hard to get it. Tuesday, April 3, 12
what most of us likely use... our standard U.S. Jan.-‐Dec. calendar which runs from Sunday to Saturday. Date.civil(2012, 4, 3) #=> Tue, 03 Apr 2012 • Ordinal date is the day of the year with the year. Date.ordinal(2012, 94) #=> Tue, 03 Apr 2012 • Commercial date starts on the first Monday on or before Jan 1 and is calculated by number of weeks and then day of week. Does anyone use this? Date.commercial(2012, 14, 2) #=> Tue, 03 Apr 2012 • Julian vs. Gregorian, modified Julian... RTFD for a history lesson. Anyone use this? Tuesday, April 3, 12
it does what it can to convert it. Time defaults to local and DateTime defaults to UTC unless you pass the zone. Date.parse("2012-04-03") # 4/3/2012 Time.parse(“20120403T190000-0500”) # 4/3/2012 7:00pm -05:00 Time.parse(“3rd Apr 2012 7:00PM”) # 4/3/2012 7:00pm -05:00 Time.parse(“3rd Apr 2012 7:00PM CST”) # 4/3/2012 8:00pm -05:00 (8pm!) Time.parse(“3rd Apr 2012 7:00PM CDT”) # 4/3/2012 7:00pm -05:00 DateTime.parse(“20120403T190000-0500”) # 4/3/2012 7:00pm -05:00 DateTime.parse(“3rd Apr 2012 7:00PM”) # 4/3/2012 7:00pm +00:00 (UTC) DateTime.parse(“3rd Apr 2012 7:00PM CST”) # 4/3/2012 7:00pm -06:00 (or 8pm -05:00!) DateTime.parse(“3rd Apr 2012 7:00PM CDT”) # 4/3/2012 7:00pm -05:00 Remember, in the rest of the world, the date format is not m/d/y but rather, d/m/y. Date.parse("4/3/2012") # 3/4/2012 Tuesday, April 3, 12
string Time.parse("") #=> ArgumentError: no time information in "" DateTime.parse("") #=> ArgumentError: invalid date • Ruby 1.8’s Time class parses an empty string as the current 1me. Time.parse("") #=> Tue Apr 03 19:00:00 -0500 2012 DateTime.parse("") #=> ArgumentError: invalid date • Although we’re not talking about it yet, Rails’ TimeWithZone returns a nil instead of an excep1on. Time.zone.parse("") #=> nil Tuesday, April 3, 12
DateTime.httpdate parses a string “according to some RFC 2616 format.” That’s what the docs say. Seriously. Ex. “Tue, 03 Apr 2012 19:00:00 CDT” • DateTime.iso8601 parses some typical ISO 8601 formats. Ex. “2012-W14-2T19:00:00-05:00” • DateTime.xmlschema parses the typical XML schema formats. Ex. “2012-04-03T19:00:00-05:00” • DateTime.strptime is the most robust, allowing you to define a template the string should match. Ex. DateTime.strptime(“4/3/2012”, “%m/%d/%Y”) # 4/3/2012 • DateTime.rfc2822, .rfc3339, .rfc822 for whoever uses these. • and DateTime.jd and .jisx0301 for those who are subject to that pain. Tuesday, April 3, 12
also gives us a way to convert from a Time to a DateTime, a DateTime to Date, etc. dt = DateTime.parse(“2012-04-03 19:00:00 CDT”) dt.class #=> DateTime dt.to_time.class #=> Time dt.to_date.class #=> Date • Conver1ng a Date to a Time converts to beginning of day local 1me. • Conver1ng a Date to a DateTime converts to UTC 1me. d = Date.parse("2012-04-03") => #<Date: 2012-04-03> d.to_time => 2012-04-03 00:00:00 -0500 d.to_datetime => #<DateTime: 2012-04-03T00:00:00+00:00> Tuesday, April 3, 12
a date is valid before you a_empt to parse it, which could raise an excep1on. Date.valid_date?(2012, 4, 3) #=> true Date.valid_date?(2012, 2, 29) #=> true Date.valid_date?(2011, 2, 29) #=> false • There are also methods for valid_commercial?, valid_ordinal? and valid_jd? Tuesday, April 3, 12
<< n -‐> date Returns a date object poin1ng n months before self Date.new(2012, 4, 3) << 1 #=> Sat, 03 Mar 2012 DateTime.new(2012, 4, 3, 19, 0, 0, ‘CDT’) << 1 #=> Sat, 03 Mar 2012 19:00:00 -0500 • d >> n Does the same but adds a month. • .cweek returns the calendar week number • .cwday returns the day of the calendar week (1-‐7, Monday being 1) • .cwyear returns the calendar week based year, while the cwyear for 1/1/2012 is 2012, its value for 1/1/2011 is 2010. • .day_fraction returns the frac1onal part of a day DateTime.new(2012,4,3,12).day_fraction #=> (1/2) • step (or upto) to provide a range dt = Date.new(2012, 4, 1) dt2 = Date.new(2012, 4, 30) dt.step(d2).select{|d| d.tuesday?}.size #=> 4 # of Tuesday’s in April 2012 Tuesday, April 3, 12
to Time, but marshaling only preserves the utc_offset. This appears to have been fixed with Ruby 1.9.3. Time.local(2012).zone != Marshal.load(Marshal.dump(Time.local(2012))).zone Rails redefines the _load and _dump methods to deal with this situa1on if you’re using a version of Ruby that has this problem. Ac1veSupport’s version preserves the zone, but the docs say it may not work in some edge cases. Tuesday, April 3, 12
• DateTime acts_like?(:date) && acts_like?(:time) • TimeWithZone acts_like?(:date) && acts_like?(:time) All this duck typing mostly so it knows it can convert a Time to a float for add/subtract. Date’s cannot be converted to a float. Tuesday, April 3, 12
also have .to_s(:db) dt.to_s(:db) #=> “2012-04-03 19:00:00” HOWEVER, it drops the 1me zone. You have to convert it to a TimeWithZone if you want it to convert a local 1me to UTC. dt.in_time_zone.to_s(:db) #=> “2012-04-04 00:00:00” Rails does this for you when persis1ng to the database or execu1ng a query. If you are using the raw value yourself, it’s easy to forget. You can tell Rails to not do this for specific models like this: class MyModel < ActiveRecord::Base self.skip_time_zone_conversion_for_attributes = [:starts_at, :ends_at, :whenever] end Tuesday, April 3, 12
to your local 1me zone by default, but allows you to convert to UTC, as well. The 1me defaults to beginning of day. d.to_time #=> 2012-04-03 00:00:00 -0500 d.to_time(:utc) #=> 2012-04-03 00:00:00 UTC • However, to_date1me doesn’t have any parameters... d.to_datetime #=> Tue, 03 Apr 2012 00:00:00 +0000 • To a TimeWithZone... (more on this class later) d.to_time_in_current_zone #=> Tue, 03 Apr 2012 00:00:00 CDT -05:00 # or use Time.zone.parse(d.to_s) NOTE: .to_time_in_current_zone is a Date method. Calling it on a DateTime object excludes the 1me. It’s like seqng it to beginning_of_day. Tuesday, April 3, 12
.to_1me dt.to_date #=> Tue, 03 Apr 2012 dt.to_time #=> Tue, 03 Apr 2012 19:00:00 -0500 DateTime’s .to_time is supposed to convert itself to a Ruby Time object or self if it’s out of range of the Ruby Time class. However, it seems to just always return self. If you really need a Time though, you can just use: Time.parse(dt.to_s) Tuesday, April 3, 12
.current method to Time that returns Time.zone.now if 1me zones are enabled, otherwise it returns Time.now. Date has a similar method. Time.now #=> 2012-04-03 19:00:00 -0500 Time.current #=> Tue, 03 Apr 2012 19:00:00 CST -05:00 Date.current #=> Tue, 03 Apr 2012 – Time zones are on by default in Rails 3. Not Rails 2. We’ll get to the config for this in a bit. Tuesday, April 3, 12
can use this format: dt.ago(3.days) #=> Tue, 27 Mar 2012 19:00:00 -0500 dt.in(1.week) #=> Tue, 10 Apr 2012 19:00:00 -0500 • Be careful with daylight savings. Even for the 3.days.ago(dt) format. # Time t.ago(1.month) #=> 2012-03-03 19:00:00 -0600 <-- takes into account DST 1.month.ago(t) #=> 2012-03-03 19:00:00 -0600 # DateTime dt.ago(1.month) #=> Sun, 04 Mar 2012 19:00:00 -0500 <-- DST AND day are wrong! 1.month.ago(dt) #=> Sat, 03 Mar 2012 19:00:00 -0500 <-- Day is right, but DST is still wrong?!? Tuesday, April 3, 12
02 Apr 2012 19:00:00 -0500 dt.tomorrow #=> Wed, 04 Apr 2012 19:00:00 -0500 • [beginning|end]_of_day, .midnight • [beginning|end]_of_week, .sunday, • [beginning|end]_of_month • [beginning|end]_of_quarter • [beginning|end]_of_year • [prev|next]_[day|week|month|year] For some reason, Time and TimeWithZone do not have [prev|next]_day methods. Also, no [prev|next]_quarter methods for any class. Tuesday, April 3, 12
one of the workhorses behind most of the beginning_of and end_of methods. • .change takes a date/1me instance and changes the pieces you want: Date.new(2012,4,3).change(:month => 6) #=> Sun, 03 Jun 2012 = same as Date.new(2012,6,3) # For the time parts (hour, min, sec, usec, any resets cascade. # Pass only the :hour, the min/sec/usec values are set to 0. DateTime.new(2012,4,3,19,30,30,‘CDT’).change(:hour => 20) #=> Tue, 03 Apr 2012 20:00:00 -0500 Tuesday, April 3, 12
but adds/subtracts the pieces you want: Date.new(2012,4,3).advance(:months => 2) #=> Sun, 03 Jun 2012 DateTime.new(2012,4,3,19,30,30,'CDT').advance(:hours => 2) #=> Tue, 03 Apr 2012 21:30:30 -0500 • Note that advance’s params are plural. Singular params are ignored. Tuesday, April 3, 12
code for Time and TimeWithZone are some useful ranges. .all_day .all_week .all_month .all_quarter .all_year tz.all_day #=> 2012-04-03 00:00:00 UTC..2012-04-03 23:59:59 UTC • These are not available for Date or DateTime objects. And since DateTime’s .to_1me method returns a DateTime, this won’t work: dt.to_time.all_day #=> NoMethodError: undefined method `all_day' Tuesday, April 3, 12
Canada)” comes from • Get a list of zones: ActiveSupport::TimeZone.all ActiveSupport::TimeZone.us_zones • Get specific 1mes ActiveSupport::TimeZone["Paris"].now #=> Wed, 3 Apr 2012 03:19:04 CEST +02:00 ActiveSupport::TimeZone["Paris"].parse('2012-04-03 19:00 CDT') #=> Wed, 04 Apr 2012 02:00:00 CEST +02:00 Tuesday, April 3, 12
current default Ac1veSupport::TimeZone object. • Rails 3 has 1me zones enabled by default. In Rails 2, you can turn this on in your environment.rb file. Rails 3, it’s in applica1on.rb if you want to change it. config.time_zone = 'Central Time (US & Canada)' # For Rails 3, the default for ActiveRecord is to store UTC times in the database. # The Rails 2 default is :local, but you can change that with this: config.active_record.default_timezone = :utc Tuesday, April 3, 12
with a 1me_zone field in it, you can change the default zone for that user’s current request with a before_filter. class ApplicationController < ActionController::Base before_filter :set_time_zone private def set_time_zone Time.zone = current_user.time_zone if current_user end end • In your views you can use Rails 1me zone helpers for a dropdown of zones. time_zone_select('user', 'time_zone', ActiveSupport::TimeZone.us_zones) Tuesday, April 3, 12
projects, we send users an email at the start of their day. It’s not 6am everywhere at the same 1me, only in certain 1mezones. def timezones_between(from_hour, to_hour) zones = Array.new ActiveSupport::TimeZone.all.each do |z| time_check = Time.current.utc + z.utc_offset.seconds zones << z if time_check.hour >= from_hour && time_check.hour < to_hour end zones end # All users who are in a timezone where it’s currently between 6am and 7am. User.where(“wants_email > 0 AND time_zone IN (?)”, timezones_between(6,7)) Tuesday, April 3, 12
Ruby 1mes are limited to UTC and the server’s local 1me zone. Represents itself as a Time class. An Ac1veSupport::TimeZone object returns true for .is_a?(Time). TimeWithZone has all the Time/DateTime methods with some addi1ons Tuesday, April 3, 12
never create a TimeWithZone directly with .new Instead, use these: Time.zone.local(2012, 4, 3, 17) Time.zone.parse(“2012-04-03 19:00”) Time.zone.at(1333497600) Time.zone.now Time.now.in_time_zone # Same as Time.current when TimeWithZone is configured. – Remember... Time.zone is the current default Ac1veSupport::TimeZone. Tuesday, April 3, 12
not the same as .to_time tz.time #=> 2012-04-03 19:00:00 UTC tz.localtime #=> 2012-04-03 19:00:00 -0500 tz.to_time #=> 2012-04-04 00:00:00 UTC tz.to_datetime #=> Tue, 03 Apr 2012 19:00:00 -0500 Tuesday, April 3, 12
TZInfo::TimezonePeriod object. Rails 4 currently appears to be spliqng this up into .period_for_local and .period_for_utc. Used so TimeWithZone instances respond like TZInfo::Timezone instances. • tz.zone is the zone’s abbrevia1on as a string. “CDT”, “PST”, etc. • tz.dst? is true if the date/1me is within observances of daylight savings 1me. • tz.utc? is only true if the date/1me is UTC. Tuesday, April 3, 12
1me to any other zone. tz.in_time_zone("Eastern Time (US & Canada)") #=> Tue, 03 Apr 2012 20:00:00 EDT -04:00 tz.in_time_zone("Tokyo") #=> Wed, 04 Apr 2012 09:00:00 JST +09:00 tz.in_time_zone("Paris") #=> Wed, 04 Apr 2012 02:00:00 CEST +02:00 # Not all zones are on the hour tz.in_time_zone("Mumbai") #=> Wed, 04 Apr 2012 05:30:00 IST +05:30 • Dump out the list of zones you can use with the following rake task: rake time:zones:all # The info from ActiveSupport::TimeZone.all Tuesday, April 3, 12
override the default 1me zone inside of the supplied block and then resets back to the exis1ng value when done. Time.zone #=> (GMT-06:00) Central Time (US & Canada) Time.use_zone(“Eastern Time (US & Canada)”) do Time.zone #=> (GMT-05:00) Eastern Time (US & Canada) end Time.zone #=> (GMT-06:00) Central Time (US & Canada) Tuesday, April 3, 12
a 1mezone on every test run. Zonebie.set_random_timezone • Timecop -‐ h_ps://github.com/jtrupiano/1mecop Provides “1me travel” and “1me freezing” to make it easier to test 1me-‐dependent code. Basically mocks Time.now, Date.today and DateTime.now with a single call. new_time = Time.local(2008, 9, 1, 12, 0, 0) Timecop.freeze(new_time) sleep(10) new_time == Time.now # ==> true Timecop.return # "turn off" Timecop Timecop.travel(new_time) sleep(10) new_time == Time.now # ==> false Tuesday, April 3, 12
can rely on javascript to convert your UTC 1mes for you in the browser using the browser’s own seqngs. There are many libraries that can do this. One is jquery-‐local@me -‐ h_p://code.google.com/p/jquery-‐local1me <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script> <script type="text/javascript" src="jquery.localtime-0.4.js"></script> <span class="localtime">2012-04-04 00:00:00Z</span> <script type="text/javascript"> $.localtime.setFormat({dateAndTime: "M-d-yyyy h:mmtt"}); </script> /* The date would display as “4/3/2012 7:00pm” assuming your browser says you’re in CDT */ Tuesday, April 3, 12