Programmierung

UTC-Zeitstempel und lokale Zeit

Für die Speicherung und Verarbeitung von Zeitpunkten (Datum & Uhrzeit) bietet sich die Nutzung von 64-Bit-Zeitstempeln an. Dies hat eine Reihe von Vorteilen gegenüber der Verwendung von zwei Werten (date und time). Zum einen kann ein Feld oder eine Variable eingespart werden, zum anderen ist die zeitliche Auflösung höher (100 Nanosekunden gegenüber 10 Millisekunden bei time) und der Wertebereich größer – date deckt nur Kalenderdaten von 1900 bis 2154 ab. Damit ein solcher Zeitstempel auch eindeutig ist, wird er als UTC-Zeit definiert – dadurch ist er unabhängig von Zeitzonen und Sommerzeitregeln.


Ein 64-Bit-Zeitstempel enthält per Definition die Anzahl von 100-Nanosekunden-Intervallen seit dem 1.1.1601 in UTC, als undefinierter Wert wird oft -1 verwendet. Um beispielsweise die aktuelle Uhrzeit in einen Zeitstempel zu bekommen, wird nur eine int64-Variable benötigt:

	tTimestamp->vmSystemTime(); 

Durch das ganzzahlige Format der Zeitstempel lassen sich Vergleiche (>, >=, <, <=, etc.) von Zeitpunkten sehr einfach durchführen. Für alle anderen Zwecke kann der Zeitstempel einfach in eine Kalenderzeit gewandelt werden:

	tCaltime # CnvCB(tTimestamp);
	// .... und wieder zurück
	tTimestamp # CnvBC(tCaltime);

Für den caltime-Wert stehen dann zahlreiche Eigenschaften (vpYear, vpMonth, vpDay, vpHours, vpMinutes, vpSeconds, vpMilliseconds, vpLeapYear, vpWeek, vpWeekYear, vpDayOfWeek, vpDate, vpTime, vpBiasMinutes) und Methoden (vmMonthModify, vmDayModify, vmSecondsModify, vmEasterDate, vmSystemTime, vmServerTime) zur Verfügung.

Eine oft wenig beachtete Eigenschaft ist vpBiasMinutes, die die zeitliche Differenz zu UTC in Minuten angibt. Da der Zeitstempel unabhängig von Zeitzonen ist, wird bei Aufruf von CnvCB() die aktuelle Zeitabweichung eingetragen. Im Falle von MEZ also 60 Minuten, bei Sommerzeit (MESZ) entsprechend 120 Minuten.

Ein Zeitpunkt wie der 05.03.2013 um 8:55 in der Zeitzone MEZ (lokale Zeit) entspricht dem 05.03.2013 7:55 UTC. Dagegen ergibt der 05.04.2013 um 8:55 MESZ den 05.04.2013 6:55 UTC. Für die Zurückrechnung der UTC-Werte in die korrekte lokale Zeit benötige ich deshalb den zu diesem Zeitpunkt gültigen BIAS. Das Betriebssystem – und somit CONZEPT 16 – liefert jedoch nur die Zeitabweichung der aktuellen Uhrzeit. Dadurch ergibt eine Umrechnung des Zeitstempels 05.03.2013 8:55 UTC unterschiedliche lokale Zeiten, je nachdem wann die Umrechnung erfolgt.

Diese Problematik gilt auch für die Umrechung einer vergangenen oder zukünftigen lokalen Zeit in UTC. Ohne Kenntnis der zu diesem Zeitpunkt gültigen Zeitzone kann sich ein falscher UTC-Wert ergeben. Oft ergibt sich dieser Fehler bei Termindaten, die als UTC gespeichert werden.

Für die Anzeige einer korrekten lokalen Zeit aus einem UTC-Zeitstempel (und umgekehrt) kann also nicht der aktuelle BIAS verwendet werden, falls eine Sommerzeitregelung besteht. Für eine globale Lösung wäre daher die Kenntnis aller Sommerzeiten in Vergangenheit und Zukunft für alle Länder und Orte (auch innerhalb eines Landes existieren unterschiedliche Regeln) notwendig. Da eine solch umfassende Lösung aber eher selten benötigt wird, haben wir eine kleines Beispiel programmiert, das eine korrekte Umrechnung von Zeitpunkten ab 1950 für Deutschland, die Schweiz und Österreich erledigt.


// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Letzter Sonntag eines Monats
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

SUB DtiMonthLastSunday
(
  aMonth                : int;        // Monat 1-12
  aYear                 : int;        // Jahr 1900-2154
)
: date;

  LOCAL
  {
    tDate               : date;
  }

{
  tDate->vpYear  # aYear;
  tDate->vpMonth # aMonth;
  tDate->vpDay   # 31;      // ergibt letzen Tag des Monats

  // wenn kein Sonntag, Datum anpassen
  if (tDate->vpDayOfWeek != 7)
    tDate->vmDayModify(-tDate->vpDayOfWeek);

  return(tDate);
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// BIAS eines bestimmten Zeitpunkts für CET ermitteln
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

SUB DtiGetBIAS
(
  aDate               : date;
  aTime               : time;
  aStdBIAS            : int32;      // BIAS der Standardzeit (0=UTC, 60=CET)
  aLocation           : alpha(2);   // DE, CH, AT
)
: int32;

  LOCAL
  {
    tCt               : caltime;    // Zeitpunkt
    tCtBegin          : caltime;    // Begin der Sommerzeit
    tCtEnd            : caltime;    // Ende der Sommerzeit
    tAdjBegin         : int32;      // Anpassungswert Begin
    tAdjEnd           : int32;      // Anpassungswert Ende
  }

{
  // Standardzeit
  tCt->vpBiasMinutes # aStdBIAS;
  tCt->vpDate        # aDate;
  tCt->vpTime        # aTime;

  // Sommerzeit
  if (tCt->vpYear >= 1980)
  {
    tCtBegin->vpBiasMinutes # aStdBIAS;
    tCtEnd->vpBiasMinutes   # aStdBIAS;

    // UTC
    tCtBegin->vpTime # 01:00:00;
    tCtEnd->vpTime   # 02:00:00;

    // Anfangstag
    if (tCt->vpYear = 1980)
    {
      // keine Sommerzeit in der Schweiz
      if (aLocation = 'CH')
        return(60);

      // abweichende Zeiten in Österreich
      if (aLocation = 'AT')
      {
        tAdjBegin # -7200;
        tAdjEnd   # -10800;
      }

      tCtBegin->vpDate # 06.04.1980;
    }
    else
      tCtBegin->vpDate # DtiMonthLastSunday(3,tCt->vpYear);

    // Endtag
    if (tCt->vpYear >= 1996)
      tCtEnd->vpDate # DtiMonthLastSunday(10,tCt->vpYear);
    else
      tCtEnd->vpDate # DtiMonthLastSunday(9,tCt->vpYear);

    // lokale Zeit
    tCtBegin->vmSecondsModify(aStdBIAS * 60 + tAdjBegin);
    tCtEnd->vmSecondsModify(aStdBIAS * 60 + tAdjEnd);

    // CEST / MESZ
    if (tCt >= tCtBegin AND tCt < tCtEnd)
      return(120);
  }

  // CET / MEZ
  return(60);
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Lokale Zeit in UTC-Zeit wandeln
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

SUB DtiLocalTimeToUTC
(
  aDate               : date;
  aTime               : time;
)
: int64

  LOCAL
  {
    tCt               : caltime;
  }

{
  // Standardzeit
  tCt->vpBiasMinutes # DtiGetBIAS(aDate,aTime,60,'DE');
  tCt->vpDate        # aDate;
  tCt->vpTime        # aTime;

  // Zeitstempel
  return(CnvBC(tCt));
}

// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// UTC-Zeit in lokale Zeit wandeln
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

SUB DtiUTCToLocalTime
(
  aTimestamp          : int64
)
: caltime

  LOCAL
  {
    tCt               : caltime;
  }

{
  tCt->vpDate        # CnvDB(aTimestamp,false);
  tCt->vpTime        # CnvTB(aTimestamp,false);
  tCt->vpBiasMinutes # DtiGetBIAS(tCt->vpDate,tCt->vpTime,0,'DE');
  tCt->vmSecondsModify(tCt->vpBiasMinutes * 60);
  return(tCt);
}

Das Beispiel kann mit wenig Aufwand für die gesamte EU erweitert werden.

Bei der Umrechnung von lokaler Zeit in UTC werden hier zwei Besonderheiten nicht berücksichtigt. Beispielsweise existiert die lokale Zeit 31.03.2013 2:44 nicht (es gibt am Tag der Umstellung auf Sommerzeit eine Lücke von 2:00 bis 3:00) und die lokale Zeit 27.10.2013 2:44 gibt es doppelt, jeweils als MEZ und MESZ.

Download

Beispielprozedur UTC.Zip (1.07 KB)
Sie müssen angemeldet sein, um die Datei herunterladen zu können.
12 Kommentare

12 Kommentare “UTC-Zeitstempel und lokale Zeit”

  1. Es tut mir leid, aber ich habe beim Testen eine neues Problem, wieder mit einer der Konvertierungsfunktionen, diesmal CnvAC gefunden.

    Ich habe Ihre Funktionen genutzt, so wie sie hier im Blog stehen und folgendes gemacht (C, CC sind caltimes):

    C->vpBiasMinutes # 0
    C->vpDate # 01.07.2007
    C->vpTime # 13:13

    C soll also ein UTC-caltime sein, das lokal in MESZ gewandeöt werden muss. Nun habe ich konvertiert:

    CC # vectorsoftUTCOrig:DtiUTCToLocalTime(CnvBC(C))
    DbgTrace(CnvAI(CC->vpBiasMinutes,_FmtInternal) + ‘ ‘ + CnvAC(CC,_FmtCaltimeRFC))

    Das DbgTrace gibt folgendes aus: "Bias: 120, Zeit: Sun, 01 Jul 2007 14:13:00 +0100". Stelle ich die Uhrzeit im OS z.B. auf den 07.05.2013 und führe dann dieselben Befehle aus, gibt das DbgTrace folgendes aus: "Bias: 120, Zeit: Sun, 01 Jul 2007 15:13:00 +0200".

    Das bedeutet, 1. dass die Konvertierungsfunktion konstant korrekt gearbeitet hat (der Bias-Wert ist in beiden Fällen korrekt 120), 2. dass CnvAC mit der Option _FmtCaltimeRFC aber einen vollständigen CalTime gemäss der aktuellen Zeitzone in einen Alpha-Wert umwandelt, statt. den im CalTime-Wert stehenden Bias zu verwenden, wie es korrekt wäre.

  2. ok, hier die korrekte Variante für alle caltime-Werte:

    SUB DtiToLocalTime
    (
    aTime : caltime
    )
    : caltime
    {
    aTime->vmSecondsModify(aTime->vpBiasMinutes * -60);
    aTime->vpBiasMinutes #
    DtiGetBIAS(aTime->vpDate,aTime->vpTime,0,’DE’);
    aTime->vmSecondsModify(aTime->vpBiasMinutes * 60);
    return(aTime);
    }

  3. Danke für die Darstellung der geänderten Funktionen, ich habe ähnliche Änderungen bereits durchgeführt und plane diese geänderten Funktionen nun in unsere Library aufzunehmen. Insgesamt eine gute Sache. Meine Beanstandung bezieht sich auf CnvCB, wie ja aus den vorigen Kommentaren deutlich geworden sein dürfte.

    Die oben stehende Version der angepassten Funktion "DtiUTCToLocalTime" mit caltime-Argument ist m. E. noch leicht fehlerhaft, da sie nicht überprüft, ob der übergebene caltime-Wert überhaupt eine UTC-Zeit darstellt. Korrekt wäre es m. E., wenn der übergebene Wert zuerst normalisiert (d.h. in einen UTC-Wert konvertiert) und erst danach in die lokale Zeit überführt wird. So werde ich ich es jedenfalls machen.

  4. Eine korrekte lokale Zeit unter Berücksichtung von Sommerzeiten kann nur auf der Grundlage entsprechender Tabellen (beispielsweise "IANA time zone database") erzeugt werden, die Windows leider nicht zur Verfügung stellt.

    Wir werden zunächst CnvCB erweitern, so daß eine Wandlung in eine UTC-caltime möglich ist. Denkbar ist auch eine Erweiterung in CONZEPT 16, um Zeitzonen-Tabellen zu verarbeiten.

  5. Ob man nun meistens die lokale Zeiten benötigt oder nicht, möchte ich nicht einschätzen. CnvCB ist sowieso nicht in der Lage, eine korrekte lokale Zeit zu produzieren, da der int64-Zeitstempel keine Zeitabweichungsinformation enthält.

    Wieso aber sollte eine Funktion etwas versuchen, zu machen, was sie gar nicht können kann?

    Die Folge ist, dass ich, wenn ich eine korrekte lokale Zeit haben will, die von CnvCB gesetzte Zeitabweichung jedesmal zurück setzen und dann irgendwelche anderen Funktionen, wie z.B. Ihre oben stehenden, verwenden muss, um eine korrekte Umrechnung durchzuführen.

    Also könnte sich CnvCB das Setzen der willkürlichen Zeitabweichung doch eigentlich gleich sparen und direkt in einen UTC-CalTime wandeln, den ich dann an eine korrekte Wandlungsfunktion übergebe, um eine lokale Zeit zu bekommen. Dadurch würde sich die Funktion dann auch konstant und vorhersagbar verhalten.

    Aus Gründen der Rückwärtskompatibilität wäre der Vorschlag, dass man CnvCB per optionalem Parameter eine von der aktuellen Zeitabweichung abweichende Zeitabweichung 🙂 mitgeben könnte, m. E. ein akzeptabler Kompromiss.

  6. Hier noch die DtiUTCToLocalTime mit caltime-Argument:

    SUB DtiUTCToLocalTime
    (
    aTime : caltime
    )
    : caltime
    {
    aTime->vpBiasMinutes #
    DtiGetBIAS(aTime->vpDate,aTime->vpTime,0,’DE’);
    aTime->vmSecondsModify(aTime->vpBiasMinutes * 60);
    return(aTime);
    }

  7. …die UTC-caltime kann anschließend direkt in einen UTC-int64-Wert gewandelt werden:

    tTimestamp # CnvBC(DtiLocalTimeToUTC(….));

    dabei wird die im caltime-Wert stehende Zeitzone verwendet und nicht die aktuelle Zone.

  8. Die Funktion DtiLocalTimeToUTC kann natürlich auch caltime anstatt int64 liefern:

    SUB DtiLocalTimeToUTC
    (
    aDate : date;
    aTime : time;
    )
    : caltime

    LOCAL
    {
    tCt : caltime;
    }

    {
    tCt->vpDate # aDate;
    tCt->vpTime # aTime;
    tCt->vpBiasMinutes # 0;
    tCt->vmSecondsModify
    (DtiGetBIAS(aDate,aTime,60,’DE’) * -60);
    return(tCt);
    }

    int64-Zeitstempel lassen sich eben gut in Datenbanken speichern, caltime-Werte braucht man doch meistens als lokale Zeit, insofern sind UTC-Werte als caltime nicht so verbreitet.

  9. Hm, ich habe ein paar Tests durchgeführt und möchte gern folgende Bemerkungen vor allem in Hinsicht auf die CnvCB-Funktion machen:

    Dadurch, dass diese Funktion "CnvCB" stets die aktuelle (also die zum Zeitpunkt der Wandlung gültige) Zeitabweichung einträgt, wie ja im Artikel auch erwähnt wird, ist die Funktion "DtiLocalTimeToUTC" leider komplizierter als nötig. Der Grund dafür ist folgender:

    Die Funktion "DtiLocalTimeToUTC" liefert einen 64-Bit Integer zurück, also muss dieser hinterher von der aufrufenden Funktion in ein CalTime gewandelt werden. Dies ist natürlich nur nötig, wenn ein CalTime gebraucht wird, aber ich vermute, das wird meistens der Fall sein.

    Bei dieser Wandlung *muss* CnvCB verwendet werden (oder geht es auch anders?). CnvCB stellt aber beim Wandeln *immer" die Zeitabweichung zum Zeitpunkt der Wandlung ein. Dementsprechend bekommt man u. U. ein völlig unerwartetes Ergebnis, z.B. ergibt
    C # CnvCB(DtiLocalTimeToUTC(01.07.2011,09:00)) =>
    C->vpDate = 01.07.2011
    C->vpTime = 08:00
    C->vpBiasMinutes = 60

    Um einen CalTime, der einem UTC-Zeitwert entspricht zu bekommen, muss man daher nun noch
    C->vmSecondsModify(-C->vpBiasMinutes * 60)
    C->vpBiasMinutes # 0
    machen. Das ist umständlich und damit rechnet man nicht unbedingt.

    Da man von einem 64-Bit Integer ja nur per CnvCB zu einem CalTime-Wert kommt, es aber eigentlich nicht gerechtfertigt ist, vorauszusetzen, dass der Anwender immer einen 64-Bit-Wert umwandeln möchte, der zu seiner momentanen Zeitabweichung passt, wäre es sinnvoll, wenn man der Funktion CnvCB diese Zeitwabweichung als optionales Argument übergeben könnte. Die korrekte Wandlung in unserem Fall sähe dann so aus: CnvCB(C,0), da auf einen UTC-Ergebnis hin gewandelt werden soll.

    So wie die Situation aber im Moment ist, ist es wohl besser mit CalTime statt mit dessen 64-Bit Repräsentation zu arbeiten, da man einen solchen Zeitstempel jederzeit mit der unverdächtigen Funktion CnvBC aus einem CalTime erzeugen kann.

  10. Ja, diese Zeitgeschichten sind immer etwas umständlich, wir haben ja darüber bereits per Mail diskutiert.

    Ich finde es sehr gut, dass Sie nun diese pragmatische Lösung für die häufigsten Situationen anbieten. Ich werde Ihre Lösung testen und dann in unsere DateTime-Library einbauen.

    Vielen Dank.

Kommentar abgeben