Konvertierung zwischen Zeitzonen mit Ruby und tzinfo

Marvin Gülker · 15.06.2016

Man steht gelegentlich vor der Aufgabe, eine in einer (beliebigen) Zeitzone vorliegende Zeitangabe in dieselbe Angabe in einer anderen Zeitzone zu konvertieren. Der Artikel zeigt, wie dies mit Ruby mithilfe des tzinfo-Gems erfolgen kann.

Kategorien: Software, Ruby

Ruby selbst besitzt nur eingeschränkte Möglichkeiten, zwischen verschiedenen Zeitzonen zu konvertieren. Die einfachste Konvertierung, die noch ohne externe Bibliotheken auskommt, ist diejenige nach UTC oder in die lokale Zeitzone.

# Nach UTC konvertieren:
t = Time.now
t.utc

# In die lokale Zeitzone konvertieren:
t = Time.now.utc
t.localtime

Das ist natürlich nicht ausreichend. Serversysteme werden üblicherweise mit UTC als Systemzeit betrieben, dennoch möchte man Zeitangaben nach außen hin in einer anderen Zeitzone darstellen. Gründe dafür können etwa Nutzereinstellungen oder die korrekte Berechnung von Zugangszeitpunkten nach deutscher Zeit sein. Abhile schafft der Gem tzinfo, der die in unixoiden Systemen enthaltene tzinfo-Datenbank auswertet und für Konvertierungen zwischen allen existenten Zeitzonen verwendet.

Installation und Einbindung erfolgen wie bei Ruby mit RubyGems üblich:

$ gem install tzinfo

und

require "tzinfo"

Doe Konvertierung von UTC in eine lokale Zeitzone ist danach denkbar einfach:

require "tzinfo"

# ...utc_time setzen...

tz = TZInfo::Timezone.get("Europe/Berlin")
german_time = tz.utc_to_local(utc_time)

Will man mit dem Ergebnis einer Konvertierung weiterarbeiten (wie meist), so ist zu beachten, dass die Zoneninfo des zurückgegebenen Zeitobjekts nicht gesetzt wird, d.h. die von Time#utc_offset zurückgegebene Information ist nicht (mehr) richtig. So wird im folgenden Beispiel die Uhrzeit zwar korrekt konvertiert, der Versatz von UTC aber fälschlich mit Null angegeben:

german_tz.utc_to_local(Time.now.utc).utc_offset
# => 0

Unter Rückgriff auf die anderen Methoden von tzinfo lässt sich das allerdings korrigieren, etwa über eine solche Methode:

def convert_to_german_time(time)
  utc = time.utc

  german_tz = TZInfo::Timezone.get("Europe/Berlin")
  german_time = german_tz.utc_to_local(utc)
  german_period = german_tz.period_for_utc(utc)

  utc_offset = german_period.offset.utc_total_offset

  Time.new(german_time.year, german_time.month,
    german_time.day, german_time.hour,
    german_time.min, german_time.sec,
    utc_offset)
end

# ...utc_time setzen...

# #utc_offset gibt den Versatz in Sekunden zurück.
puts convert_to_german_time(utc_time).utc_offset / 60.0 / 60.0
# => 2.0

Da man die Zeitzoneninfo bei einer bestehenden Instanz von Time scheinbar nicht ändern kann, ist eine elegantere Möglichkeit wohl nicht gegeben.

Von besonderem Interesse kann es sein, ob in der Zielzeitzone zurzeit die Sommerzeit (engl. „daylight saving time“ = Tageslichtsicherungszeit, DST) aktiv ist, d.h. die Zeitzone einen unüblichen Versatz im Vergleich zu UTC aufweist. Dies lässt sich wie folgt ermitteln:

require "tzinfo"

utc_time = Time.now.utc
german_tz = TZInfo::Timezone.get("Europe/Berlin")
german_time = german_tz.utc_to_local(utc_time)
german_period = german_tz.period_for_local(german_time)

if german_period.dst?
  # #utc_total_offset gibt den Versatz in Sekunden zurück.
  offset = german_period.offset.utc_total_offset / 60.0 / 60.0
  puts "DST in effect with offset of #{offset} hours."
else
  puts "No DST in effect."
end

Praktisch ist auch, dass tzinfo die Namen sämtlicher Zeitzonen kennt (da es ja mit der tzinfo-Datenbank arbeitet):

tz = TZInfo::Timezone.get("Asia/Seoul")
period = tz.period_for_utc(Time.now.utc)
puts period.offset.abbreviation #=> KST

Die tzinfo-Bibliothek bietet daneben noch einige weitere Möglichkeiten. Diesbezüglich empfiehlt sich ein Blick in die ausführliche Dokumentation.