views:

73

answers:

2

Hi all. when users sign up to one of my sites for a free trial, i set their account expiry to be "14.days.from_now". Then on the home page i show how many days they have remaining, which i get with:

(user.trial_expires - Time.now)/86400 

(because there are 86400 seconds in a day, ie 60 * 60 * 24)

The funny thing is, this comes out as more than 14, so gets rounded up to 15. On closer investigation in the console this happens for just two days in the future (if you know what i mean). eg

>> Time.now
=> Fri Oct 29 11:09:26 0100 2010
>> future_1_day = 1.day.from_now
=> Sat, 30 Oct 2010 11:09:27 BST 01:00
#ten past eleven tomorrow

>> (future_1_day - Time.now)/86400
=> 0.999782301526931
#less than 1, what you'd expect right?

>> future_2_day = 2.day.from_now
=> Sun, 31 Oct 2010 11:09:52 GMT 00:00
>> (future_2_day - Time.now)/86400
=> 2.04162248861183
#greater than 2 - why?

I thought maybe it was to do with timezones - i noticed that the time from 1.day from now was in BST and the time 2 days from now was in GMT. So, i tried using localtime and got the same results!

>> future_2_day = 2.day.from_now.localtime
=> Sun Oct 31 11:11:24 0000 2010
>> (future_2_day - Time.now)/86400
=> 2.04160829127315
>> (future_2_day - Time.now.localtime)/86400
=> 2.04058651585648

I then wondered how big the difference is, and it turns out that it is exactly an hour out. So it looks like some time zone weirdness, or at least something to do with time zones that i don't understand. Currently my time zone is BST (british summer time) which is one hour later than UTC at the moment (till this sunday at which point it reverts to the same as UTC).

The extra hour seems to be introduced when i add two days to Time.now: check this out. I start with Time.now, add two days to it, subtract Time.now, then subtract two days of seconds from the result, and am left with an hour.

It just occurred to me, in a head slapping moment, that this is occurring BECAUSE the clocks go back on sunday morning: ie at 11.20 on sunday morning it will be two days AND an extra hour from now. I was about to delete all of this post, but then i noticed this: i thought 'ah, i can fix this by using (24*daynum).hours instead of daynum.days, but i still get the same result: even when i use seconds!

>> (Time.now + (2*24).hours - Time.now) - 86400*2
=> 3599.99969500001
>> (Time.now + (2*24*3600).seconds - Time.now) - 86400*2
=> 3599.999855

So now i'm confused again. How can now plus two days worth of seconds, minus now, minus two days worth of seconds be an hour worth of seconds? Where does the extra hour sneak in?

+4  A: 

As willcodejavaforfood has commented, this is due to daylight saving time which ends this weekend.

When adding a duration ActiveSupport has some code in it to compensate for if the starting time is in DST and the resulting time isn't (or vice versa).

def since(seconds)
  f = seconds.since(self)
  if ActiveSupport::Duration === seconds
    f
  else
    initial_dst = self.dst? ? 1 : 0
    final_dst   = f.dst? ? 1 : 0
    (seconds.abs >= 86400 && initial_dst != final_dst) ? f + (initial_dst - final_dst).hours : f
  end
rescue
  self.to_datetime.since(seconds)
end

If you have 11:09:27 and add a number of days you will still get 11:09:27 on the resulting day even if the DST has changed. This results in an extra hour when you come to do calculations in seconds.

A couple of ideas:

  • Use the distance_of_time_in_words helper method to give the user an indication of how long is left in their trial.
  • Calculate the expiry as Time.now + (14 * 86400) instead of using 14.days.from_now - but some users might claim that they have lost an hour of their trial.
  • Set trials to expire at 23:59:59 on the expiry day regardless of the actual signup time.
mikej
aha, that's great, thanks mike. I knew it had to be something like that. I did try setting the expiry time using seconds (see the end of my post) but got the same results. Also, when i try distance_of_time_in_words i get an error: "TypeError: ActiveSupport::TimeWithZone can't be coerced into Fixnum". time_ago_in_words works fine.
Max Williams
What parameters were you passing to `distance_of_time_in_words`? `time_ago_in_words` is just a wrapper around `distance_of_time_in_words` with the 2nd parameter set to `Time.now` i.e. it does `distance_of_time_in_words(from_time, Time.now, include_seconds)`
mikej
+1  A: 

You could use the Date class to calculate the number of days between today and the expire date.

expire_date = Date.new(user.trial_expires.year, user.trial_expires.month, user.trial_expires.day)
now = Time.now
today = Date.new(now.year, now.month, now.day)
days_until_expiration = (expire_date - today).to_i
Jonas Elfström