Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Slightly Less Painful Time Zones (RailsConf)

Slightly Less Painful Time Zones (RailsConf)

Abstract:
For developers, there are two things that are certain for time zones: you can’t avoid having to deal with them, and you will screw them up at some point. There are, however, some ways to mitigate the pain. This talk will discuss tactics for avoiding time zone mayhem, using a feature to send out weekly email reports in a customer’s local time zone as a case study. It will cover idiosyncrasies of how time zones are handled in Ruby and Rails, how to write tests to avoid false positives, and advice on how to release time zone-related code changes more safely.

This talk was presented at RailsConf 2015 in Atlanta, ~30 minutes: https://www.youtube.com/watch?v=VFDurYw6aZ8
Blog post: http://kwugirl.blogspot.com/2015/04/slightly-less-painful-time-zones.html

Katherine Wu

April 21, 2015
Tweet

More Decks by Katherine Wu

Other Decks in Programming

Transcript

  1. ! @kwugirl “in October 1582, since days 5..14 just do

    not exist.” http://guides.rubyonrails.org/v4.0.13/active_support_core_extensions.html#extensions-to-date
  2. ! @kwugirl falsehoods programmers believe about time • There are

    only 24 time zones. http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
  3. ! @kwugirl falsehoods programmers believe about time • There are

    only 24 time zones. • Time zones always differ by a whole hour. http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
  4. ! @kwugirl falsehoods programmers believe about time • There are

    only 24 time zones. • Time zones always differ by a whole hour. • Time always goes forwards. http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
  5. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015
  6. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month
  7. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month => Mon, 09 Mar 2015
  8. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month => Mon, 09 Mar 2015 [4] pry(main)> Date.today - 30.days
  9. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month => Mon, 09 Mar 2015 [4] pry(main)> Date.today - 30.days => Tue, 10 Mar 2015
  10. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… enqueues
  11. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… email for account 1 email for account 2 email for account 3… enqueues generates
  12. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… email for account 1 email for account 2 email for account 3… enqueues SCHEDULING generates
  13. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… email for account 1 email for account 2 email for account 3… enqueues SCHEDULING EXECUTING generates
  14. ! @kwugirl cron job enqueues email for account 1 email

    for account 2 email for account 3… generates WeeklyEmailJob for account 1 WeeklyEmailJob for account 2 WeeklyEmailJob for account 3…
  15. ! @kwugirl cron job enqueues Monday 10am Pacific time email

    for account 1 email for account 2 email for account 3… generates WeeklyEmailJob for account 1 WeeklyEmailJob for account 2 WeeklyEmailJob for account 3…
  16. ! @kwugirl set up report jobs to run Monday 1:01am

    local time Local time Pacific time WeeklyEmailJob for account 1 Mon 1:01am JST Sun 9:01am WeeklyEmailJob for account 2 Mon 1:01am EDT Sun 10:01pm WeeklyEmailJob for account 3 Mon 1:01am HAST Mon 4:01am
  17. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… enqueues email for account 1 email for account 2 email for account 3… generates local
  18. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… enqueues Saturday 10am Pacific time email for account 1 email for account 2 email for account 3… generates local
  19. ! @kwugirl report processing spread out 0 25 50 75

    100 8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT before
  20. ! @kwugirl report processing spread out 0 25 50 75

    100 8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT before 0 25 50 75 100 8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT after
  21. ! @kwugirl [1] pry(main)> Date.today => Sun, 05 Apr 2015

    [2] pry(main)> DateTime.parse("Monday, 1:01")
  22. ! @kwugirl [1] pry(main)> Date.today => Sun, 05 Apr 2015

    [2] pry(main)> DateTime.parse("Monday, 1:01") => Mon, 06 Apr 2015 01:01:00 +0000
  23. ! @kwugirl { "International Date Line West" => "Pacific/Midway", "Midway

    Island" => "Pacific/Midway", "American Samoa" => "Pacific/Pago_Pago", "Hawaii" => "Pacific/Honolulu", "Alaska" => "America/Juneau", "Pacific Time (US & Canada)" => "America/Los_Angeles", "Tijuana" => "America/Tijuana", "Mountain Time (US & Canada)" => "America/Denver", "Arizona" => "America/Phoenix", "Chihuahua" => "America/Chihuahua", "Mazatlan" => "America/Mazatlan", "Central Time (US & Canada)" => "America/Chicago", "Saskatchewan" => "America/Regina", "Guadalajara" => "America/Mexico_City", "Mexico City" => "America/Mexico_City", "Monterrey" => "America/Monterrey", "Central America" => "America/Guatemala", "Eastern Time (US & Canada)" => "America/New_York", "Indiana (East)" => "America/Indiana/Indianapolis", "Bogota" => "America/Bogota", "Lima" => "America/Lima", "Quito" => "America/Lima", "Atlantic Time (Canada)" => "America/Halifax", "Caracas" => "America/Caracas", "La Paz" => "America/La_Paz", "Santiago" => "America/Santiago", "Newfoundland" => "America/St_Johns", "Brasilia" => "America/Sao_Paulo", "Buenos Aires" => "America/Argentina/Buenos_Aires", "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", "Azores" => "Atlantic/Azores", "Cape Verde Is." => "Atlantic/Cape_Verde", "Dublin" => "Europe/ Dublin", "Edinburgh" => "Europe/London", "Lisbon" => "Europe/Lisbon", "London" => "Europe/London", "Casablanca" => "Africa/Casablanca", "Monrovia" => "Africa/Monrovia", "UTC" => "Etc/UTC", "Belgrade" => "Europe/Belgrade", "Bratislava" => "Europe/Bratislava", "Budapest" => "Europe/Budapest", "Ljubljana" => "Europe/Ljubljana", "Prague" => "Europe/Prague", "Sarajevo" => "Europe/Sarajevo", "Skopje" => "Europe/Skopje", "Warsaw" => "Europe/Warsaw", "Zagreb" => "Europe/Zagreb", "Brussels" => "Europe/Brussels", "Copenhagen" => "Europe/Copenhagen", "Madrid" => "Europe/Madrid", "Paris" => "Europe/Paris", "Amsterdam" => "Europe/Amsterdam", "Berlin" => "Europe/Berlin", "Bern" => "Europe/ Berlin", "Rome" => "Europe/Rome", "Stockholm" => "Europe/Stockholm", "Vienna" => "Europe/Vienna", "West Central Africa" => "Africa/Algiers", "Bucharest" => "Europe/Bucharest", "Cairo" => "Africa/Cairo", "Helsinki" => "Europe/ Helsinki", "Kyiv" => "Europe/Kiev", "Riga" => "Europe/Riga", "Sofia" => "Europe/Sofia", "Tallinn" => "Europe/Tallinn", "Vilnius" => "Europe/Vilnius", "Athens" => "Europe/Athens", "Istanbul" => "Europe/Istanbul", "Minsk" => "Europe/ Minsk", "Jerusalem" => "Asia/Jerusalem", "Harare" => "Africa/Harare", "Pretoria" => "Africa/Johannesburg", "Kaliningrad" => "Europe/Kaliningrad", "Moscow" => "Europe/Moscow", "St. Petersburg" => "Europe/Moscow", "Volgograd" => "Europe/Volgograd", "Samara" => "Europe/Samara", "Kuwait" => "Asia/Kuwait", "Riyadh" => "Asia/Riyadh", "Nairobi" => "Africa/Nairobi", "Baghdad" => "Asia/Baghdad", "Tehran" => "Asia/Tehran", "Abu Dhabi" => "Asia/Muscat", "Muscat" => "Asia/Muscat", "Baku" => "Asia/Baku", "Tbilisi" => "Asia/Tbilisi", "Yerevan" => "Asia/Yerevan", "Kabul" => "Asia/Kabul", "Ekaterinburg" => "Asia/Yekaterinburg", "Islamabad" => "Asia/Karachi", "Karachi" => "Asia/Karachi", "Tashkent" => "Asia/Tashkent", "Chennai" => "Asia/Kolkata", "Kolkata" => "Asia/Kolkata", "Mumbai" => "Asia/Kolkata", "New Delhi" => "Asia/Kolkata", "Kathmandu" => "Asia/Kathmandu", "Astana" => "Asia/Dhaka", "Dhaka" => "Asia/Dhaka", "Sri Jayawardenepura" => "Asia/Colombo", "Almaty" => "Asia/Almaty", "Novosibirsk" => "Asia/Novosibirsk", "Rangoon" => "Asia/Rangoon", "Bangkok" => "Asia/Bangkok", "Hanoi" => "Asia/Bangkok", "Jakarta" => "Asia/Jakarta", "Krasnoyarsk" => "Asia/Krasnoyarsk", "Beijing" => "Asia/Shanghai", "Chongqing" => "Asia/Chongqing", "Hong Kong" => "Asia/Hong_Kong", "Urumqi" => "Asia/Urumqi", "Kuala Lumpur" => "Asia/Kuala_Lumpur", "Singapore" => "Asia/Singapore", "Taipei" => "Asia/ Taipei", "Perth" => "Australia/Perth", "Irkutsk" => "Asia/Irkutsk", "Ulaanbaatar" => "Asia/Ulaanbaatar", "Seoul" => "Asia/Seoul", "Osaka" => "Asia/Tokyo", "Sapporo" => "Asia/Tokyo", "Tokyo" => "Asia/Tokyo", "Yakutsk" => "Asia/ Yakutsk", "Darwin" => "Australia/Darwin", "Adelaide" => "Australia/Adelaide", "Canberra" => "Australia/Melbourne", "Melbourne" => "Australia/Melbourne", "Sydney" => "Australia/Sydney", "Brisbane" => "Australia/Brisbane", "Hobart" => "Australia/Hobart", "Vladivostok" => "Asia/Vladivostok", "Guam" => "Pacific/Guam", "Port Moresby" => "Pacific/Port_Moresby", "Magadan" => "Asia/Magadan", "Srednekolymsk" => "Asia/Srednekolymsk", "Solomon Is." => "Pacific/ Guadalcanal", "New Caledonia" => "Pacific/Noumea", "Fiji" => "Pacific/Fiji", "Kamchatka" => "Asia/Kamchatka", "Marshall Is." => "Pacific/Majuro", "Auckland" => "Pacific/Auckland", "Wellington" => "Pacific/Auckland", "Nuku'alofa" => "Pacific/Tongatapu", "Tokelau Is." => "Pacific/Fakaofo", "Chatham Is." => "Pacific/Chatham", "Samoa" => "Pacific/Apia" } ~24 bands vs…
  24. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact! => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"]
  25. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact! => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"] [2] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name if z.formatted_offset == "-11:00"}.compact!
  26. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact! => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"] [2] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name if z.formatted_offset == "-11:00"}.compact! => ["International Date Line West", "Midway Island", "American Samoa"]
  27. ! @kwugirl (no UTC -12:00 offset) “The Baker and Howland

    islands are not there. 
 But as (wikipedia says) they are uninhabited, they would be a quite specific use case. 
 I don't think they're going to be added to TZInfo.” — dmathieu
 https://github.com/rails/rails/issues/11390
  28. ! @kwugirl ActiveSupport::TimeZone ! local_to_utc(time, dst=true) Adjust the given time

    to the simultaneous time in UTC. Returns a Time.utc() instance. !
  29. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”) default = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
  30. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”) default = ActiveSupport::TimeZone['Pacific Time (US & Canada)'] local_time_zone = account_time_zone || default

  31. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”) default = ActiveSupport::TimeZone['Pacific Time (US & Canada)'] local_time_zone = account_time_zone || default
 local_time_zone.local_to_utc(dt) end
  32. ! @kwugirl context "enqueue time" do setup do
 saturday_pt =

    DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")
 Timecop.freeze(saturday_pt)
 end

  33. ! @kwugirl context "enqueue time" do setup do
 saturday_pt =

    DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 # your tests

  34. ! @kwugirl context "enqueue time" do setup do
 saturday_pt =

    DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 # your tests
 teardown do
 Timecop.return
 end
 end
  35. ! @kwugirl # Running: ! .. ! Finished in -37452054.639617s,

    -0.0000 runs/s, -0.0000 assertions/s. ! 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  36. ! @kwugirl # Running: ! .. ! Finished in -37452054.639617s,

    -0.0000 runs/s, -0.0000 assertions/s. ! 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  37. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end

  38. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do
  39. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
  40. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
  41. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") assert_equal expected, local_1am(mtn_tz) end
  42. ! @kwugirl # Running: ! .. ! Finished in 0.059444s,

    33.6451 runs/s, 33.6451 assertions/s ! 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
  43. ! @kwugirl before cron job enqueues Monday 10am Pacific time

    email for account 1 email for account 2 email for account 3… generates WeeklyEmailJob for account 1 WeeklyEmailJob for account 2 WeeklyEmailJob for account 3…
  44. ! @kwugirl after cron job WeeklyEmailJob for account 1 WeeklyEmailJob

    for account 2 WeeklyEmailJob for account 3… enqueues Saturday 10am Pacific time email for account 1 email for account 2 email for account 3… generates local
  45. ! @kwugirl start on Sat morning if pdt_day == SATURDAY

    base_email = 'script/run_weekly_email_report.rb' Async::Command::Bulk.new(base_email).enqueue end
  46. ! @kwugirl [1] pry(main)> Date.today can’t tell whether it’s closest

    date or last date Sun Mon Tues Wed Thurs Fri Sat
  47. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  48. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  49. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  50. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  51. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  52. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  53. ! @kwugirl changed test setup saturday_pt = DateTime.parse("1st May 2015

    10:05:00 AM -07:00") Timecop.freeze(saturday_pt)
  54. ! @kwugirl changed test setup saturday_pt = DateTime.parse("1st May 2015

    10:05:00 AM -07:00") Timecop.freeze(saturday_pt) setting date to May 1st (Fri)! want to get May 4th (next Mon)
  55. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    [2] pry(#<SchedulingHelperTest>)> DateTime.parse("Monday, 1:01") date set to May 1st
  56. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    [2] pry(#<SchedulingHelperTest>)> DateTime.parse("Monday, 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 date set to May 1st
  57. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    [2] pry(#<SchedulingHelperTest>)> DateTime.parse("Monday, 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 date set to May 1st DateTime parses to Apr 6th?!?
  58. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  59. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  60. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end Test only passed because happened to run in the same week of Apr 4th, not because of Timecop.freeze
  61. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  62. ! @kwugirl setup do
 saturday_pt = DateTime.parse("8th Feb 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("10th Feb 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  63. ! @kwugirl setup do
 saturday_pt = DateTime.parse("8th Feb 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("10th Feb 2015 01:01:00 AM -06:00") assert_wday(expected, "Monday") ! assert_equal expected, local_1am(mtn_tz) end
  64. ! @kwugirl # Running: ! FF ! Finished in 0.061686s,

    32.4223 runs/s, 64.8445 assertions/s. ! 1) Failure: SchedulingHelperTest#test_: enqueue time should be the next Monday 1:01am local time (west of UTC). -Mon, 10 Feb 2015 01:01:00 -0600 +Mon, 06 Apr 2015 07:01:00 +0000 ! 2) Failure: SchedulingHelperTest#test_: enqueue time should be the next Monday 1:01am local time (east of UTC). -Mon, 10 Feb 2015 01:01:00 +0700 +Sun, 05 Apr 2015 18:01:00 +0000 ! ! 2 runs, 4 assertions, 2 failures, 0 errors, 0 skips
  65. ! @kwugirl multiple time zones • the time we’re used

    to thinking of (Pacific time) • server’s time zone (who even knows)
  66. ! @kwugirl multiple time zones • the time we’re used

    to thinking of (Pacific time) • server’s time zone (who even knows) • the customer’s time zone (could be ahead of us or behind us when running script)
  67. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday

  68. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']
  69. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.formatted_offset
  70. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.formatted_offset like “+07:00”
  71. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.formatted_offset 
 DateTime.parse("#{next_monday} 01:01:00 AM #{current_tz_offset}") end
  72. ! @kwugirl # Running: ! .. ! Finished in 0.041562s,

    48.1209 runs/s, 96.2418 assertions/s. ! 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  73. ! @kwugirl context "enqueue time" do context "during U.S. standard

    time" do… end ! context "during U.S. daylight saving" do… end
  74. ! @kwugirl # Running: ! ..F. ! Finished in 0.061256s,

    65.2997 runs/s, 130.5995 assertions/s. ! 1) Failure: SchedulingHelperTest#test_: enqueue time during U.S. daylight saving should be the next Monday 1:01am local time (west of UTC). --- expected +++ actual @@ -1 +1 @@ -Mon, 09 Jun 2014 01:01:00 -0600 +Mon, 09 Jun 2014 01:01:00 -0700 ! ! 4 runs, 8 assertions, 1 failures, 0 errors, 0 skips
  75. ! @kwugirl # Running: ! ..F. ! Finished in 0.061256s,

    65.2997 runs/s, 130.5995 assertions/s. ! 1) Failure: SchedulingHelperTest#test_: enqueue time during U.S. daylight saving should be the next Monday 1:01am local time (west of UTC). --- expected +++ actual @@ -1 +1 @@ -Mon, 09 Jun 2014 01:01:00 -0600 +Mon, 09 Jun 2014 01:01:00 -0700 ! ! 4 runs, 8 assertions, 1 failures, 0 errors, 0 skips off by 1…
  76. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.now.formatted_offset 
 DateTime.parse("#{next_monday} 01:01:00 AM #{current_tz_offset}") end
  77. ! @kwugirl # Running: ! .... ! Finished in 0.043776s,

    91.3743 runs/s, 182.7485 assertions/s. ! 4 runs, 8 assertions, 0 failures, 0 errors, 0 skips
  78. ! @kwugirl context "when run on different days of the

    week" do setup do @samoa_tz = ActiveSupport::TimeZone[“American Samoa"] @wellington_tz = ActiveSupport::TimeZone["Wellington"] end
  79. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday")
  80. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday")
  81. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday") # It's already Monday in the customer account's time zone
 assert_wday(@wellington_tz.now, "Monday")

  82. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday") # It's already Monday in the customer account's time zone
 assert_wday(@wellington_tz.now, "Monday")
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")

  83. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday") # It's already Monday in the customer account's time zone
 assert_wday(@wellington_tz.now, "Monday")
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")
 assert_equal expected, local_1am(local_tz)
 end
  84. ! @kwugirl # Running: ! ..... ! Finished in 0.059667s,

    83.7984 runs/s, 201.1162 assertions/s. ! 5 runs, 12 assertions, 0 failures, 0 errors, 0 skips
  85. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday")
  86. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

  87. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday")
  88. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday") 
 assert_equal expected, local_1am(local_tz) end
  89. ! @kwugirl # Running: ! ...... ! Finished in 0.059173s,

    101.3976 runs/s, 253.4940 assertions/s. ! 6 runs, 15 assertions, 0 failures, 0 errors, 0 skips
  90. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday")
  91. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

  92. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("9th Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday")

  93. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("9th Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday")
 assert_equal expected, local_1am(local_tz) end
  94. ! @kwugirl # Running: ! ....... ! Finished in 0.060848s,

    115.0408 runs/s, 295.8191 assertions/s. ! 7 runs, 18 assertions, 0 failures, 0 errors, 0 skips
  95. ! @kwugirl tests — Before • context "enqueue time" •

    should "be the next Monday 1:01am local time (west of UTC)" • should "be the next Monday 1:01am local time (east of UTC)"
  96. ! @kwugirl tests — After • context "enqueue time" •

    context "during U.S. standard time” • should "be the next Monday 1:01am local time (west of UTC)" • should "be the next Monday 1:01am local time (east of UTC)" • context "during U.S. daylight saving” • should "be the next Monday 1:01am local time (west of UTC)" • should "be the next Monday 1:01am local time (east of UTC)” • context "when run on different days of the week" • should "be the next day when run on late Sunday night" • should "be the same week when still Monday somewhere" • should "be the next week when run on Tuesday"
  97. ! @kwugirl tests to write • covering time zones west/east

    of UTC • pin down exactly when you’re starting and when you’re ending • pick random dates in the past or in the future--not current dates • test dates inside/outside daylight saving period • check that the dates being used in your tests are the expected day of the week • test when triggering the scripts on different days of the week • test on dates of daylight saving transitions
  98. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb"
  99. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb" 
 options = " --account 3,10 --enqueue_later"
  100. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb" 
 options = " --account 3,10 --enqueue_later" recipient = " --to \"[email protected]\""
  101. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb" 
 options = " --account 3,10 --enqueue_later" recipient = " --to \"[email protected]\"" 
 test_email = [base, options, recipient].join("") Async::Command::Bulk.new(test_email).enqueue end
  102. ! @kwugirl lessons 1. Trust NOTHING! 2. Have a backup

    plan! 3. Do an internal test run! 4. Today’s console results are not 
 tomorrow’s console results ! 5. Guard against writing false positive tests! 6. Figure out a consistent, global logic