ruby
38 lines · 8 steps
Converting timestamps across time zones with TZInfo
A module that parses ISO 8601 strings, shifts them between named zones via TZInfo, and formats the result back with a correct offset.
Explained by
highlit
1require "time"
2require "tzinfo"
3
4module TimeConverter
5 module_function
6
7 def convert(iso_string, to_zone:, from_zone: nil)
8 parsed = Time.iso8601(iso_string)
9
10 if parsed.utc_offset.zero? && from_zone && !iso_string.match?(/[Zz]|[+-]\d{2}:?\d{2}\z/)
11 source = TZInfo::Timezone.get(from_zone)
12 parsed = source.local_to_utc(parsed) { |periods| periods.first }
13 end
14
15 target = TZInfo::Timezone.get(to_zone)
16 localized = target.utc_to_local(parsed.utc)
17 offset = target.observed_utc_offset(localized)
18
19 Time.new(
20 localized.year, localized.month, localized.day,
21 localized.hour, localized.min, localized.sec, offset
22 )
23 end
24
25 def to_iso(time, zone:)
26 tz = TZInfo::Timezone.get(zone)
27 period = tz.period_for_utc(time.utc)
28 local = tz.utc_to_local(time.utc)
29 sign = period.observed_utc_offset.negative? ? "-" : "+"
30 abs = period.observed_utc_offset.abs
31 format(
32 "%sT%s%s%02d:%02d",
33 local.strftime("%Y-%m-%d"),
34 local.strftime("%H:%M:%S"),
35 sign, abs / 3600, (abs % 3600) / 60
36 )
37 end
38end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A naive timestamp with no offset is ambiguous, so you must attach a source zone before converting.
- 2Round-tripping through UTC is the reliable pivot for moving a moment between named zones.
- 3DST means an offset must be observed for a specific instant, not assumed constant for a zone.
Related explainers
ruby
class Product < ApplicationRecord belongs_to :account after_save :record_audit_trail
Building an audit trail with Rails callbacks
callbacks
audit-logging
dirty-tracking
Intermediate
7 steps
ruby
class User < ApplicationRecord store_accessor :preferences, :theme, :timezone,
Storing user settings in a JSON column with Rails
store_accessor
json-columns
validations
Intermediate
7 steps
ruby
class Subscription < ApplicationRecord belongs_to :account enum :status, {
Modeling subscription lifecycle in Rails
enums
scopes
state-management
Intermediate
8 steps
ruby
class InactiveAccountCleanup BATCH_SIZE = 500 INACTIVITY_THRESHOLD = 18.months
Batch-processing dormant users in Rails
batch-processing
activerecord
service-object
Intermediate
9 steps
ruby
class ArticleSerializer < ActiveModel::Serializer attributes :id, :title, :slug, :excerpt, :reading_time, :published_at, :url belongs_to :author, serializer: UserSerializer
Shaping API output with ActiveModel::Serializer in Rails
serialization
json api
computed attributes
Intermediate
9 steps
ruby
def collatz_lengths Enumerator.new do |y| n = 1 loop do
Building infinite sequences with lazy enumerators
lazy-evaluation
enumerators
infinite-sequences
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/converting-timestamps-across-time-zones-with-tzinfo-explained-ruby-9dd9/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.