timecop: Timecop.freeze with Dates is lossy and users should be warned

Realistically, ActiveSupport’s Time.zone being set and Timecop.freeze-ing with Date objects are not compatible. Date is a lossy representation of Time in Ruby anyway since it doesn’t store offset, and the assumptions we make to handle that are surprising.

I think that freezing to Date objects should be deprecated.

Consider the following:

require 'active_support/all'

# The following timezones were chosen because they will always
# exemplify this bug; other timezone combinations may only show
# it during certain parts of the day.
ENV['TZ'] = 'Pacific/Kiritimati' # UTC+14:00
Time.zone = 'Midway Island'      # UTC-11:00

Timecop.freeze do # prevent drift
  puts Date.today #=> 2013-07-19
  # Date.today is generated using ENV['TZ'] or the computer's clock, 
  # independent of Time.zone.

  Timecop.freeze(Date.today) do
    # But in freezing, we used Time.zone and *assumed* that the date given was in
    # the timezone set for ActiveSupport, then froze time to that day's beginning
    # timestamp.
    # Here we encounter drift.
    puts Date.today #=> 2013-07-20

    Timecop.freeze(Date.today) do
      # And we can drift again.
      puts Date.today #=> 2013-07-21
    end
  end
end

About this issue

  • Original URL
  • State: closed
  • Created 11 years ago
  • Comments: 18 (6 by maintainers)

Commits related to this issue

Most upvoted comments

@brokenladder because of the interplay of ENV['TZ'] and Time.zone, more setup was required to exemplify the bug:

> Time.now.utc_offset # ENV['TZ'] is nil, but computer's clock is UTC.
# => 0
> Time.zone = -2 # manually set Time.zone to negative offset. 2 bypasses AS's DST issues
# => -2
> Timecop.freeze('2013-04-24'.to_date) { Date.today }
# => Wed, 24 Apr 2013
> Time.zone = 2 # manually set Time.zone to positive offset. 2 bypasses AS's DST issues
# => 2
> Timecop.freeze('2013-04-24'.to_date) { Date.today }
# => Tue, 23 Apr 2013

My point though, was not just that there is a bug, but that the assumptions inherent in making the conversion from Date to a Time or an ActiveSupport::TimeWithZone object make supporting Date as an input faulty at best.

Date is a lossy representation of Time in Ruby anyway since it doesn’t store offset

This comment makes no sense to me. Offset is irrelevant for the purposes of a date.

A date is defined as the period between a midnight and the following midnight, so moment-in-time data (such as the timestamp at the beginning of that day) cannot be extracted without additional information or assumptions about the UTC-offset; it is, therefore a lossy representation of time. Timecop is built to freeze the time, not the date, and using Date as an input forces Timecop to make assumptions about the UTC-offset, in this case using the one provided by Time.zone (where Date.today does not).

the two will always agree with each other

The first example you gave does not go through Time.parse, since it is supplying a Date object to Timecop.freeze, and is not representative of the context in which it was quoted. ActiveSupport’s String#to_date makes no assumptions about timezone because Date objects don’t need this, but it forces Timecop to make an assumption when converting the provided Date object to a Time.

Timecop.freeze('2013-04-24'.to_date) { Date.today }
#=> Tue, 23 Apr 2013

In the second example (which does go through Time.parse), the same assumptions are being made about utc-offset by both Time.parse and Date.today, the two will always agree, regardless of timezone settings in Time.zone or ENV['TZ'].

Timecop.freeze('2013-04-24') { Date.today }
=> Wed, 24 Apr 2013