Skip to content

Instantly share code, notes, and snippets.

@wdiechmann
Created November 13, 2012 14:21
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save wdiechmann/4065991 to your computer and use it in GitHub Desktop.
Save wdiechmann/4065991 to your computer and use it in GitHub Desktop.
time and time again, your rails will come clean :)

Started out with that frustrating issue that bites many a Rails developer (judging from just a tad over 24hrs googling and reading blogs etc) - time_zone!

I'm on

ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin11.4.2] - Rails 3.2.8
config.time_zone = 'Copenhagen'

and initially, I wanted to bark up this three, 'with the pack'

[31] pry(main)> ent.enter_at=Time.now 
=> 2012-11-13 11:52:45 +0100
[32] pry(main)> ent.save
   (0.1ms)  BEGIN
   (0.4ms)  UPDATE `entrances` SET `enter_at` = '2012-11-13 10:52:45', `updated_at` = '2012-11-13 10:52:55' WHERE `entrances`.`id` = 1
  SQL (0.2ms)  INSERT INTO `versions` (`created_at`, `event`, `item_id`, `item_type`, `object`, `whodunnit`) VALUES ('2012-11-13 10:52:55', 'update', 1, 'Entrance', '---\nox_id: 1\nemployee_id: 9\nwage_item_id: !!null \nenter_at: 2012-11-13 08:02:00.000000000Z\nleft_at: 2012-10-11 21:21:00.000000000Z\nreason: tyuio\nstate: drafted\nactive: true\ncreated_at: 2012-10-11 09:02:52.000000000Z\nupdated_at: 2012-11-13 06:39:07.000000000Z\nid: 1\n', NULL)
   (0.5ms)  COMMIT
=> true
[34] pry(main)> reload!
Reloading...
=> true
[35] pry(main)> Entrance.first
  Entrance Load (0.3ms)  SELECT `entrances`.* FROM `entrances` WHERE (entrances.ox_id='1') LIMIT 1
=> #<Entrance id: 1, ox_id: 1, employee_id: 9, wage_item_id: nil, enter_at: "2012-11-13 10:52:45", left_at: "2012-10-11 21:21:00", reason: "tyuio", state: "drafted", active: true, created_at: "2012-10-11 09:02:52", updated_at: "2012-11-13 10:52:55">
[37] pry(main)> Entrance.first.enter_at.to_s(:entrance)
  Entrance Load (0.4ms)  SELECT `entrances`.* FROM `entrances` WHERE (entrances.ox_id='1') LIMIT 1
=> "2012-11-13 11:52:45 +0100"

Everything is dandy ;)

[39] pry(main)> date=Date.today
=> Tue, 13 Nov 2012
[40] pry(main)> date=Date.today.to_datetime
=> Tue, 13 Nov 2012 00:00:00 +0000
[44] pry(main)> date += 1.hour + 30.minutes
=> Tue, 13 Nov 2012 01:30:00 +0000
[45] pry(main)> ent.enter_at
=> Tue, 13 Nov 2012 11:52:45 CET +01:00
[46] pry(main)> ent.enter_at = date
=> Tue, 13 Nov 2012 01:30:00 +0000
[47] pry(main)> ent.save
   (0.2ms)  BEGIN
   (0.6ms)  UPDATE `entrances` SET `enter_at` = '2012-11-13 01:30:00', `updated_at` = '2012-11-13 11:27:08' WHERE `entrances`.`id` = 1
  SQL (0.3ms)  INSERT INTO `versions` (`created_at`, `event`, `item_id`, `item_type`, `object`, `whodunnit`) VALUES ('2012-11-13 11:27:08', 'update', 1, 'Entrance', '---\nox_id: 1\nemployee_id: 9\nwage_item_id: !!null \nenter_at: 2012-11-13 10:52:45.000000000Z\nleft_at: 2012-10-11 21:21:00.000000000Z\nreason: tyuio\nstate: drafted\nactive: true\ncreated_at: 2012-10-11 09:02:52.000000000Z\nupdated_at: 2012-11-13 10:52:55.000000000Z\nid: 1\n', NULL)
   (0.6ms)  COMMIT
=> true
[48] pry(main)> reload!
Reloading...
=> true
[49] pry(main)> ent=Entrance.first
  Entrance Load (0.5ms)  SELECT `entrances`.* FROM `entrances` WHERE (entrances.ox_id='1') LIMIT 1
=> #<Entrance id: 1, ox_id: 1, employee_id: 9, wage_item_id: nil, enter_at: "2012-11-13 01:30:00", left_at: "2012-10-11 21:21:00", reason: "tyuio", state: "drafted", active: true, created_at: "2012-10-11 09:02:52", updated_at: "2012-11-13 11:27:08">
[50] pry(main)> ent.enter_at
=> Tue, 13 Nov 2012 02:30:00 CET +01:00
[51] pry(main)> date.class
=> DateTime
[52] pry(main)> ent.enter_at.class
=> ActiveSupport::TimeWithZone

Which is very much not dandy - but you see, then I took a very good look at date

[64] pry(main)> date.to_time
=> 2012-11-13 01:30:00 UTC
[65] pry(main)> date.in_time_zone
=> Tue, 13 Nov 2012 02:30:00 CET +01:00

and all at once I realized that the DB really was doing exactly what I was telling it to! It stored a point in time (in another time_zone) and was kind enough to show it correctly to me, once I asked for it.

Likewise - once I started storing the date/time correctly, everything was/is back to 'dandy'

[66] pry(main)> ent.enter_at = date.in_time_zone
=> Tue, 13 Nov 2012 02:30:00 CET +01:00
[67] pry(main)> ent.save
   (0.1ms)  BEGIN
   (0.1ms)  COMMIT
=> true
[68] pry(main)> ent.reload
  Entrance Load (0.3ms)  SELECT `entrances`.* FROM `entrances` WHERE `entrances`.`id` = 1 LIMIT 1
=> #<Entrance id: 1, ox_id: 1, employee_id: 9, wage_item_id: nil, enter_at: "2012-11-13 01:30:00", left_at: "2012-10-11 21:21:00", reason: "tyuio", state: "drafted", active: true, created_at: "2012-10-11 09:02:52", updated_at: "2012-11-13 11:27:08">
[69] pry(main)> ent.enter_at
=> Tue, 13 Nov 2012 02:30:00 CET +01:00
[70] pry(main)> 

But wait - wasn't it 1 1/2 hr I wanted to add to 'start_of_day'? Sure - my offset was just - well - off ;)

[70] pry(main)> date=Date.today.to_datetime.in_time_zone
=> Tue, 13 Nov 2012 01:00:00 CET +01:00
[71] pry(main)> date+=1.hour+30.minutes
=> Tue, 13 Nov 2012 02:30:00 CET +01:00

In my opinion Rails is doing the right thing - I just had to "get up to speed on how to use it" <:(

I hope somebody will find this post sooner than I did :)

Cheers

ps.: Eventually I fabricated a small module to serve my DB right :)

#
# copylefted by design
# ALCO Company
# Walther Diechmann
# nov 2012
#
# usage:
#
#   Add the following line to your controller,model, whatever 
#   
#		include TimeZoneIo
# 
#	call the module from somewhere like:
#	
#		@some_active_record_model_instance.some_datetime_field = build_time_in_zone Time.now, { null_as_nil: true, add_days: 1, add_hours: 3, add_seconds: 45, subtract_months: 2 }
#
module TimeZoneIo
  extend ActiveSupport::Concern

  included do

	#
	# construct a time object
	# and add/subtract time(range)
	#
    def build_time_in_zone start, options
      tiz = time_zoned( Time.parse( start ).to_datetime, options ) || time_zoned(Time.now.at_beginning_of_day, options)
      return tiz if options.empty?
      delta = 0
      options.each do |k,v|
        next if v.blank?
        op, range = k.to_s.split("_")
        oper = "v.to_i.#{range}"
        case op
        when 'add';       delta += eval(oper)
        when 'subtract';  delta -= eval(oper)
        end
      end
      return nil if ( (delta==0) && (options.include?( :null_as_nil )) )
      tiz += delta unless delta==0
      return tiz
    end

	#
	# make the time present itself in the correct time_zone
	#
    def time_zoned tm, options
      @usr_tz ||= (options.delete(:time_zone) || Time.zone)
      tm.in_time_zone @usr_tz
    end

  end

end        
@MrHubble
Copy link

MrHubble commented Aug 5, 2013

Thanks for the great gist. This datetime stuff is doing my head in.

When you say "once I started storing the date/time correctly", do you mean once you started using .in_time_zone ? I have tried this solution and it doesn't seem to be working for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment