Skip to content

Instantly share code, notes, and snippets.

@bf4
Created September 7, 2012 18:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bf4/3668333 to your computer and use it in GitHub Desktop.
Save bf4/3668333 to your computer and use it in GitHub Desktop.
Ruby Time and Timezone walkthrough
require 'pathname'
root = Pathname File.expand_path('..', __FILE__)
require root.join('timezone_converter')
require 'erb'
@converter = TimezoneConverter.new
@template = ERB.new(root.join("index.html.erb").read)
# def time_in_zone(time, zone)
# TimezoneConverter::BadZone
run ->(env){
request = Rack::Request.new(env)
time, zone = request.params.values_at("time", "zone")
if time and zone
begin
@time = "#{@converter.time_in_zone(time, zone)}, is localtime #{time} in #{zone}"
rescue TimezoneConverter::BadZone => e
@time = e.inspect
end
else
@time = ''
end
@local_time = Time.now.localtime
@local_zone = @local_time.zone || 'Unknown'
[ 200,
{ 'Content-Type' => 'text/html', }, [@template.result(binding)]
]
}
source "https://rubygems.org"
gem "rack"
gem "tzinfo"
GEM
remote: https://rubygems.org/
specs:
rack (1.5.2)
thread_safe (0.3.4)
tzinfo (1.2.1)
thread_safe (~> 0.1)
PLATFORMS
ruby
DEPENDENCIES
rack
tzinfo
<html>
<body>
It is now <%= @local_time %> in <%= @local_zone %><br>
<%= @time %><br>
<form>
<label for="time">Local time:</label>
<input name="time" type="text">
<label for"zone">Zone you want the time converted to:</label>
<input name="zone" type="text">
<input type="submit">
</form>
<p>Known zones:</p>
<% @converter.zones_mapping.each do |simple_zone| %>
<%= simple_zone.to_s %><br>
<% end %>
</body>
</html>
web: bundle exec rackup config.ru -p $PORT
# see https://gist.github.com/bf4/3668333
require 'active_support/time'
# require 'active_support/core_ext/time'
# require 'active_support/core_ext/date'
# require 'active_support/core_ext/date_time'
# require 'active_support/time_with_zone'
class RailsSimpleZone
def self.all
ActiveSupport::TimeZone::MAPPING.values.map do |tz_info_name|
tz = ActiveSupport::TimeZone[tz_info_name]
new(tz)
end
end
def rails_friendly_name_zone_from_tz(tz='America/Chicago')
ActiveSupport::TimeZone::MAPPING.detect {|rails_zone_key,tz_info_name| tz_info_name == tz }.first
end
def self.tz_offset(tzinfo)
if tzinfo.respond_to?(:formatted_offset)
tzinfo.formatted_offset
else
total_offset = tzinfo.to_i
offset = '%.2d:00' % total_offset
offset.start_with?('-') ? offset : "+#{offset}"
end
end
attr_reader :name, :offset
def initialize(tz)
@name = tz.name
@offset = self.class.tz_offset(tz)
end
def to_str
"Zone: #{name}. Offset: #{offset}"
end
def to_s
to_str
end
def inspect
"<#SimpleZone #{name}. Offset: #{offset}>"
end
end
class RailsTimezoneConverter
BadZone = Class.new(StandardError)
def time_in_zone(time, zone)
zone = Zone(zone)
time = Time(time)
zone.parse(time)
end
def Zone(zone)
if zone.respond_to?(:formatted_offset)
zone
elsif zone.respond_to?(:current_period)
ActiveSupport::TimeZone[zone.name]
else
ActiveSupport::TimeZone[zone] || guess_tz(zone)
end
end
def Time(time)
if time.respond_to?(:strftime)
time
else
Time.parse(time)
end
end
def guess_tz(zone_guess)
raise bad_zone_error(zone_guess) if zone_guess.to_s.size.zero?
guess = zone_guess.to_s.split('/')[-1].downcase
zm = zones_mapping.find do |tz|
tz.name.downcase.include?(guess) ||
matching_offset?(tz.offset, guess)
end
if zm
ActiveSupport::TimeZone[zm.name]
else
raise bad_zone_error(zone_guess)
end
end
def bad_zone_error(zone)
BadZone.new("Need a zone to convert to. Got #{zone.inspect}")
end
def matching_offset?(reference,guess)
reference == (guess.to_i.zero? ? guess : RailsSimpleZone.tz_offset(guess))
end
def zones_mapping
@zones_mapping ||= RailsSimpleZone.all
end
end
if $0 == __FILE__
require 'rspec/autorun'
describe RailsTimezoneConverter do
context 'Zone()' do
let(:tz) { ActiveSupport::TimeZone['America/Chicago'] }
example do
expect(tz.name).to eq('America/Chicago')
end
it 'returns the time zone passed in' do
expect(subject.Zone(tz)).to eq(tz)
end
it 'returns the timezone for a valid identifier' do
expect(subject.Zone(tz.name)).to eq(tz)
end
it 'guessed the timezone from an imprecise identifier' do
expect(subject.Zone('Chicago')).to eq(tz)
end
end
context 'Time()' do
let(:time) { Time.now }
example do
expect(time).to respond_to(:strftime)
end
it 'returns the time passed in' do
expect(subject.Time(time)).to eq(time)
end
it 'returns a parsed time object when passed a string' do
time_string = time.xmlschema
expect(subject.Time(time_string).xmlschema).to eq(time_string)
end
end
describe "#tz_offset" do
it 'returns a UTC offset of +00:00 for UTC' do
tz = ActiveSupport::TimeZone['UTC']
expected = '+00:00'
expect(RailsSimpleZone.tz_offset(tz)).to eq(expected)
end
it 'returns +00:00 for +00:00' do
offset = '+00:00'
expected = '+00:00'
expect(RailsSimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns +00:00 for 0' do
offset = 0
expected = '+00:00'
expect(RailsSimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns +05:00 for 5' do
offset = 5
expected = '+05:00'
expect(RailsSimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns -05:00 for -5' do
offset = -5
expected = '-05:00'
expect(RailsSimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns +00:00 for -0' do
offset = -0
expected = '+00:00'
expect(RailsSimpleZone.tz_offset(offset)).to eq(expected)
end
end
describe '#matching_offset?' do
it 'matches 5 to +05:00' do
reference = '+05:00'
guess = 5
expect(
subject.matching_offset?(reference,guess)
).to be_true
end
end
describe "#guess_tz" do
let(:tz) { ActiveSupport::TimeZone['America/Chicago'] }
it 'returns a timezone with a matching identifier' do
expect(subject.guess_tz('Chicago')).to eq(tz)
end
it 'returns a timezone with a matching offset' do
offset = '-05:00'
tz = subject.guess_tz('-05:00')
expect(RailsSimpleZone.tz_offset(tz)).to eq(offset)
end
end
describe "#zones_mapping" do
it 'is an array of objects with the tz name and offset' do
expect(subject.zones_mapping.
find{|zm| zm.name == 'Etc/UTC'}.
offset
).to eq('+00:00')
end
end
end
end
=begin
see
http://time.is/
http://strfti.me/
=end
#
=begin
t = Time.now
# fails undefined method `xmlschema'
t.xmlschema
require 'time'
# or require 'tzinfo'
# works "2012-09-07T08:32:03-05:00"
t.xmlschema
=end
# Time.local('America/Chicago')
=begin
# http://tzinfo.rubyforge.org/doc/files/README.html
# to get the right xmlschema
require 'time'
require 'rubygems'
require 'tzinfo'
def time_in_zone(time,zone)
tzinfo = zone.respond_to?(:current_period) ? zone : TZInfo::Timezone.get(zone) rescue guess_tz(zone)
offset = tz_offset(tzinfo)
if RUBY_VERSION < '1.9'
tzinfo.utc_to_local(time.utc)
else
time.localtime(offset)
end
end
def tz_offset(tzinfo)
'%.02d:00' % (tzinfo.current_period.utc_total_offset / 60 / 60 ) # e.g. '-06:00'
end
def guess_tz(zone_guess)
guess = zone_guess.to_s.split('/')[-1]
TZInfo::Timezone.us_zones.detect {|tz| tz.name =~ /#{guess}/i }
end
start_datetime = '2012-11-03T10:00:00-06:00'
time = Time.parse(start_datetime)
tzinfo = guess_tz('honolulu')
offset = tz_offset(tzinfo)
localtime = time_in_zone(time,guess_tz('honolulu'))
localtime.xmlschema
=> "2012-11-03T06:00:00-10:00" # and the time object is changed, too
tzinfo.utc_to_local(time.utc).xmlschema
=> "2012-11-03T06:00:00Z"
# Note that the Time returned will look like it is UTC (Time.zone will return "UTC"). This is because it is not currently possible to change the offset of an individual Time instance.
tzinfo.current_period.utc_total_offset
tzinfo.current_period.utc_offset === utc_offset + std_offset.
http://stackoverflow.com/questions/9962038/how-do-i-calculate-the-offset-in-hours-of-a-given-timezone-from-utc-in-ruby
current.dst?
#=> true
To get the base offset of the timezone from UTC in seconds:
current.utc_offset
#=> -28800 which is -8 hours; this does NOT include daylight savings
To get the daylight savings offset from standard time:
current.std_offset
#=> 3600 which is 1 hour; this is because right now we're in daylight savings
To get the total offset from UTC:
current.utc_total_offset
#=> -25200 which is -7 hours
Time.mktime(sec, min, hour, day, month, year, wday, yday, isdst, tz)
Time.utc(sec, min, hour, day, month, year, wday, yday, isdst, tz)
Time.gm(sec, min, hour, day, month, year, wday, yday, isdst, tz)
Time.local(sec, min, hour, day, month, year, wday, yday, isdst, tz)
Time.new(year, month=nil, day=nil, hour=nil, min=nil, sec=nil, utc_offset=nil)
Time.new(2007,11,5,13,45,0, "-05:00") # EST (Detroit)
starts_at.localtime('-06:00')
Time.zone_offset('CDT')
tzinfo.current_period.zone_identifier == :PDT
DateTime rfc2822, rfc3339, rfc3339, rfc822
starts_at.gmt_offset
starts_at.getlocal
=end
class ShowTime
def timezone_from_tz(tz='America/Chicago')
TZInfo::Timezone.get(tz)
end
# utc = tz.local_to_utc(local)
# period = tz.period_for_utc(DateTime.new(2005,8,29,15,35,0))
# id = period.zone_identifier
# us = TZInfo::Country.get('US')
# timezones = us.zone_identifiers
# The zone_info method of Country provides an additional description and location for each Timezone in the Country.
def demonstrate(time)
puts "#{desc('default:')}\t#{time}"
[:to_i, :utc, :xmlschema, :rfc2822, :httpdate, :iso8601].each do |meth|
puts "#{desc meth}:\t#{time.method(meth).call}"
end
describe_time(time)
# Rails Time.zone
demonstrate_zone(time,Time.zone) if defined?(TZInfo)
end
def describe_time(time)
puts "#{desc 'Zone:'}\t#{time.zone.inspect}"
puts "#{desc 'Strftime:'}\t#{time.strftime("%c").inspect}"
end
def desc(s)
"%10s" % s.to_s
end
require 'timecop'
def with_time(time,&block)
Timecop.freeze(time)
block.call
Timecop.return # "turn off" Timecop
end
def midnight_in_chicago
Time.local(2012, 03, 28, 0, 0, 0)
end
def spring_forward_2012
Time.local(2012, 03, 10, 2, 0, 0)
end
def fall_back_2012
Time.local(2012, 11, 03, 2, 0, 0)
end
module Rails
# activesupport: Time.zone
def demonstrate_rails_start_of_day(time)
puts 3.days.from_now.utc.beginning_of_day
puts Time.now.utc.beginning_of_day + 3.days #end_of_day
end
def get_tzinfo_zone_from_rails(tz ='America/Chicago')
zone = rails_friendly_name_zone_from_tz(tz)
Time.zone = zone
# Time.zone.class => ActiveSupport::TimeZone
Time.zone
end
def rails_friendly_name_zone_from_tz(tz='America/Chicago')
ActiveSupport::TimeZone::MAPPING.detect {|rails_zone_key,tz_info_name| tz_info_name == tz }.first
end
def demonstrate_zone(time,tz_info)
Time.zone = nil
puts "I got #{time.xmlschema}\t zone #{Time.zone.inspect}"
puts "\tsetting Time.zone to #{tz_info}"
Time.zone = tz_info
puts "\tHey, Time.zone #{Time.zone.inspect}"
rails_friendly_name = rails_friendly_name_from_tz(tz_info)
puts "\tsetting Time.zone to #{rails_friendly_name}"
Time.zone = rails_friendly_name
puts "I got #{time.xmlschema}\t zone #{Time.zone.inspect}"
puts "Getting the ActiveSupport::Timezone"
zone = the_zone_i_need(rails_friendly_name)
puts "Trying one.parse(time)"
puts zone.parse(time).xmlschema
end
def demonstrate_to_s(time,format = :db)
time.to_s(format) #uses utc.to_s(:db)
end
def the_zone_i_need(rails_friendly_name)
Time.zone = rails_friendly_name
Time.zone
end
# * Create ActiveSupport::TimeWithZone instances via TimeZone's +local+, +parse+, +at+ and +now+ methods.
# * t.to_s(:rfc822)
# * def to_s(format = :default)
# if format == :db
# utc.to_s(format)
# * ActiveSupport::TimeWithZone
# begin
# @time_zone.period_for_local(@time)
# rescue ::TZInfo::PeriodNotFound
# # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
# @time += 1.hour
# retry
# end
# end
end
end
# # leap second
=begin
* 12 a.m. Wednesday March 28th in Chicago is Tuesday March 27th in Denver and Palo Alto
* 2004-04-01T16:32:45-06:00
* Note that iso8601 is the both human-readable and machine, unambiguous, and sortable as a string. It is the format used in the microformats
* UTC 'timezone' designator is "Z" e.g 1994-11-05T13:15:30Z corresponds to 1994-11-05T08:15:30-05:00
=end
=begin
time = midnight_in_chicago
ShowTime::Rails.demonstrate_rails_beginning_of_day(time)
tz = 'America/Phoenix'
tzinf = timezone_from_tz(tz)
rails_friendly_name = ShowTime::Rails.rails_friendly_name_zone_from_tz(tz)
zone = Showtime::Rails.get_tzinfo_zone_from_rails(tz)
Time.zone = zone
Time.zone.parse("2012-09-07 13:15")
# Time.zone = zone
# ActiveSupport::TimeWithZone.new(rails_friendly_name)
# Showtime:Rails.demonstrate
=end
# make it 10am in chicago, palo alto, madrid
# Time
# - require 'time'; Time.parse("30/12/2001") .. in 1.9.2 parses as dd/mm/yyyy .. was mm/dd/yyyy in 1.8!
=begin
TODO:
Date vs. Time vs. DateTime
with require 'time'
with require 'tzinfo'
with require 'activesupport'
Compare 1.hour to Date.civil(
ActiveSupport
rails 3.1
require 'active_support/time/autoload.rb'
loads 'active_support/duration'
loads 'active_support/time_with_zone'
loads 'active_support/values/time_zone'
rails 2.3
require 'active_support/values/time_zone.rb'
ActiveSupport::TimeZone
active_support/core_ext/time/behavior
active_support/core_ext/time/calculations
active_support/core_ext/time/conversions
=end
# see https://gist.github.com/bf4/3668333
# http://tzinfo.rubyforge.org/doc/files/README.html
# to get the right xmlschema
require 'time'
require 'rubygems'
require 'tzinfo'
class SimpleZone
def self.all
TZInfo::Timezone.all_country_zones.map do |tz|
new(tz)
end
end
def self.tz_offset(tzinfo)
if tzinfo.respond_to?(:current_period)
total_offset = (tzinfo.current_period.utc_total_offset / 60 / 60 ) # e.g. '-06:00'
else
total_offset = tzinfo.to_i
end
offset = '%.2d:00' % total_offset
offset.start_with?('-') ? offset : "+#{offset}"
end
attr_reader :name, :offset
def initialize(tz)
@name = tz.name
@offset = self.class.tz_offset(tz)
end
def to_str
"Zone: #{name}. Offset: #{offset}"
end
def to_s
to_str
end
def inspect
"<#SimpleZone #{name}. Offset: #{offset}>"
end
end
class TimezoneConverter
BadZone = Class.new(StandardError)
def time_in_zone(time, zone)
zone = Zone(zone)
time = Time(time)
offset = SimpleZone.tz_offset(zone)
if RUBY_VERSION < '1.9'
zone.utc_to_local(time.utc)
else
time.localtime(offset)
end
end
def Zone(zone)
if zone.respond_to?(:current_period)
zone
else
begin
TZInfo::Timezone.get(zone)
rescue TZInfo::InvalidTimezoneIdentifier
guess_tz(zone)
end
end
end
def Time(time)
if time.respond_to?(:strftime)
time
else
Time.parse(time)
end
end
def guess_tz(zone_guess)
raise bad_zone_error(zone_guess) if zone_guess.to_s.size.zero?
guess = zone_guess.to_s.split('/')[-1].downcase
zm = zones_mapping.find do |tz|
tz.name.downcase.include?(guess) ||
matching_offset?(tz.offset, guess)
end
if zm
TZInfo::Timezone.get(zm.name)
else
raise bad_zone_error(zone_guess)
end
end
def bad_zone_error(zone)
BadZone.new("Need a zone to convert to. Got #{zone.inspect}")
end
# if guess is text, match exact, else try to convert to offset
def matching_offset?(reference,guess)
reference == (guess.to_i.zero? ? guess : SimpleZone.tz_offset(guess))
end
def zones_mapping
@zones_mapping ||= SimpleZone.all
end
end
if $0 == __FILE__
require 'rspec/autorun'
describe TimezoneConverter do
context 'Zone()' do
let(:tz) { TZInfo::Timezone.get('America/Chicago') }
example do
expect(tz.name).to eq('America/Chicago')
end
it 'returns the time zone passed in' do
expect(subject.Zone(tz)).to eq(tz)
end
it 'returns the timezone for a valid identifier' do
expect(subject.Zone(tz.name)).to eq(tz)
end
it 'guessed the timezone from an imprecise identifier' do
expect(subject.Zone('Chicago')).to eq(tz)
end
end
context 'Time()' do
let(:time) { Time.now }
example do
expect(time).to respond_to(:strftime)
end
it 'returns the time passed in' do
expect(subject.Time(time)).to eq(time)
end
it 'returns a parsed time object when passed a string' do
time_string = time.xmlschema
expect(subject.Time(time_string).xmlschema).to eq(time_string)
end
end
describe "#tz_offset" do
it 'returns a UTC offset of +00:00 for UTC' do
tz = TZInfo::Timezone.get('UTC')
expected = '+00:00'
expect(SimpleZone.tz_offset(tz)).to eq(expected)
end
it 'returns +00:00 for +00:00' do
offset = '+00:00'
expected = '+00:00'
expect(SimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns +00:00 for 0' do
offset = 0
expected = '+00:00'
expect(SimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns +05:00 for 5' do
offset = 5
expected = '+05:00'
expect(SimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns -05:00 for -5' do
offset = -5
expected = '-05:00'
expect(SimpleZone.tz_offset(offset)).to eq(expected)
end
it 'returns +00:00 for -0' do
offset = -0
expected = '+00:00'
expect(SimpleZone.tz_offset(offset)).to eq(expected)
end
end
describe '#matching_offset?' do
it 'matches 5 to +05:00' do
reference = '+05:00'
guess = 5
expect(
subject.matching_offset?(reference,guess)
).to be_true
end
end
describe "#guess_tz" do
let(:tz) { TZInfo::Timezone.get('America/Chicago') }
it 'returns a timezone with a matching identifier' do
expect(subject.guess_tz('Chicago')).to eq(tz)
end
it 'returns a timezone with a matching offset' do
offset = '-05:00'
tz = subject.guess_tz('-05:00')
expect(SimpleZone.tz_offset(tz)).to eq(offset)
end
end
describe "#zones_mapping" do
it 'is an array of objects with the tz name and offset' do
expect(subject.zones_mapping.
find{|zm| zm.name == 'America/Chicago'}.
offset
).to eq('-05:00')
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment