Zur Homepage www.HI-Tier.de DateTime - Java Date Ersatz
Zurück Home Nach oben
Öffentlicher Bereich für Entwickler

 

-> Offene bzw. zu klärende bzw. festzulegende Punkte

Problem

Die Konvertierung von Datum-, Zeit- und Timestamp-Angaben zwischen einer Datenbank, Servern und Clients funktioniert in Java nicht reibungslos. Es werden teilweise unterschiedliche Timezones berücksichtigt, die das Ergebnis dann um eine Stunde verfälschen (beim Datum an 0 Uhr Grenzen sogar um einen Tag). Diesem sollte mit einer einheitlichen Klasse abgeholfen werden.

Nebenbei hilft die Umsetzung der internen Date-Klassen beim Cross Compilen (von Java nach C#, um dann z.B. ein HitBatch.exe zu erzeugen). C# kennt das Objekt java.util.Date (und die davon abgeleiteten Klassen java.sql.Date, java.sql.Time und java.sql.Timestamp) nicht.

Lösung

Eine oder mehrere Java-Klassen, die die Generierung, Transformierung, Parsing und Formatieren eines Zeitstempels übernehmen. Die Klasse DateTime wird so geschrieben, daß keinerlei java.util.Date plus deren abgeleitete Klassen vorkommen (um die Portierbarkeit zu gewährleisten). Zusätzlich gibt es eine Klasse DateTimeExtended, die eine Schnittstelle zwischen den java.util.Date Klassen und DateTime darstellt. Als dritte Klasse hilft DateTimeFormat beim Parsen und Formatieren.

Ansatz

Um das hauptsächliche Problem der Timezones aus der Welt zu schaffen, wird eine Basis festgelegt. Wir entschieden uns für die "jetzt gültige lokale Uhrzeit". Wenn wir 11:15 Uhr haben, dann ist 11:15 Uhr mitteleuropäische (Sommer-)Zeit gemeint. Umgerechnet auf Greenwich wäre das 10:15 Uhr (Winterzeit) oder 9:15 Uhr (Sommerzeit). Für den Datenbankzugriff muß dann natürlich die Timezone des DB-Hosts berücksichtigt werden - sprich: beim temporären Generieren eines java.sql.Timestamp muß beim Anlegen des Objekts unsere "fehlende" Zeitzone berücksichtigt werden. Gleiches gilt für das Auslesen von Zeitwerten aus der DB.

Um korrekte Zeitberechnungen durchzuführen, ist das Umsetzen des Gregorianischen Kalenders mit all seinen Regeln notwendig. Als Startbasis wurde der 1.1.0001 verwendet. Intern werden somit die Anzahl der Tage seit dem 1.1.0001, die Anzahl der Sekunden seit Tagesbeginn und die Anzahl der Mikrosekunden seit Sekundenbeginn mitgeführt.

Datumsregeln

Der Gregorianische Kalender gilt seit dem 15. Oktober 1582. Direkt davor gibt es eine Lücke von 10 Tagen, d.h. nach dem 04.10.1582 folgte direkt der 15.10.1582 (auf Anordnung des Papstes Gregor XIII, daher der Name). Bis zum 04.10.1582 galt der Julianische Kalender. Ein Jahr 0 hat es nie gegeben (d.h. vor dem 1.1.0001 kommt der 1.1.-0001 bzw. 1.1.0001 v.Chr.).

Der Julianische Kalender hat für die Schaltjahre eine einfache Regel: alle 4 Jahre im Februar einen Tag mehr. Mit dem Gregorianischen Kalender wurde eine andere Regelung eingeführt: Der 4-Jahres-Abstand bleibt, aber alle 100 Jahre fällt das Schaltjahr aus und alle 400 Jahre findet es doch statt (jeweils zu jedem vollen Jahrhundert). Im Jahr 1900 gab es somit kein Schaltjahr, jedoch aber im Jahr 2000.

Ein ganzer Tag dauert 24 Stunden, umgerechnet 86400 Sekunden (24 Stunden * 60 Minuten * 60 Sekunden). Strenggenommen fehlen trotz der neuen Schalttagsregel jährlich 27 Sekunden, die in einigen Tausend Jahren zu einer signifikanten Größe heranwachsen werden (in 100 Jahren sind dies schon 45 Minuten), aber die Tatsache wird bei heutigen Datums-Implementierungen ignoriert.

Berechnungen

Anhand der intern gespeicherten Anzahl der Tage seit 1.1.0001 lassen sich leicht Datumsberechnungen durchführen. Bei tageweiser Addition bzw. Subtraktion genügt das Anpassen der Anzahl Tage. Bei monats- und jahresweiser Addition bzw. Subtraktion werden ausgehend vom aktuellen Monat und Jahr blockweise Operationen durchgeführt.

An Schaltjahresgrenzen gibt es zusätzliche Flags, die das Verhalten steuern. Möglich sind dann Einstellungen wie Exception werfen, vor die Grenze setzen oder nach die Grenze.

Konstruktoren

Die einfachsten Konstruktoren sind die, die aus einer Datumsangabe oder einem vollständigen Timestamp ein DateTime anlegen:

DateTime(day,month,year);
DateTime(day,month,year,hour,minute,second);
DateTime(day,month,year,hour,minute,second,micros);

Intern werden aus diesen Angaben die seriellen Zahlen für Tage, Sekunden und Mikrosekunden ermittelt. Damit ist das DateTime-Objekt dann bereit für weitere Operationen. Vorteilhaft ist hier die direkte Angabe der Monatsnummer 1 bis 12 statt den Monaten 0 bis 11, wie man es von UNIX bzw java.util.Date her kennt. Die Jahreszahl wird hier unverändert übernommen, d.h. 98 bleibt das Jahr 98 und wird nicht zu 1998. Die Reihenfolge der Parameter ist der "europäischen" nachempfunden, d.h. beginnend mit Tag statt mit Jahr.

Als weiteren Konstruktor gibt es

DateTime(millis,timezone_diff);

In einem Java-System sind die millis die Millisekunden seit dem 1.1.1970 00:00 Uhr UTC. Dieses Zählsystem wird beibehalten, um kein Chaos anzurichten. Intern wird erst die Zeitzone von UTC auf unsere "fehlende" Zeitzone korrigiert (anhand des zweiten Parameters timezone_diff) und dann auf die seriellen Angaben aufgeteilt. timezone_diff ist relativ zu UTC zu sehen: für die mitteleuropäische Zeitzone sind +3600000 (Winterzeit) bzw +7200000 (Sommerzeit) Millisekunden anzusetzen.

Der Konstruktor ohne Zeitzonenangabe findet sich in der erweiterten Klasse:

DateTimeExtended(millis);

Intern wird die Zeitzone anhand eines lokalen java.util.Date Objektes ermittelt (Erläuterung s.u.) und das DateTime via super(millis,local_timezone_diff) angelegt.

Der Konstruktor ohne Parameter

DateTimeExtended();

verwendet den Konstruktor DateTimeExtended(millis) mit millis = System.currentTimeMillis(), um das DateTime zu bilden.

Als letzten Konstruktor gibt es den Konvertierer:

DateTimeExtened(date);

date ist ein Objekt von java.util.Date (oder seiner abgeleiteten Klassen). Intern wird dessen millis ermittelt und dessen Zeitzone via mitgerechnet und das Objekt anhand von DateTime(millis,timezone_diff) angelegt.

Umwandlung UNIX millis <=> DateTime

Die "seriellen Werte" des DateTime sind im folgenden die drei wesentlichen Komponenten des DateTime: Tage seit 1.1.0001, Sekunden seit MItternacht und Mikrosekunden seit Sekundenbeginn.

java.util.Date (und abgeleitete Klassen) nach DateTime

Extrahieren der UNIX millis aus Date, extrahieren des timezone offset (und Umrechnen in Millisekunden) und schließlich subtrahieren des timezone offset von millis und dann die Millisekunden in die seriellen Werte umwandeln. Es wird subtrahiert, weil Date.getTimezoneOffset() einen Wert liefert, den man zur lokalen Zeit addieren muß, um UTC zu erhalten. Für UTC+1 liefert sie beispielsweise -60 Minuten.

Verwendung: Konstruktor DateTimeExtended(Date) und die statischen Methoden DateTimeExtended.getSqlXXX(recordset,column) für Daten aus einer Datenbank

UNIX millis nach DateTime

Die Umwandlung ist etwas problematisch: Da nicht bekannt ist, in welcher Timezone das DateTime erwartet wird, wird dem Wandler mitgeteilt, welcher timezone offset für das UNIX millis benötigt wird. Daher der Konstruktor in DateTime mit zwei Parametern.

Die erweiterte Klasse DateTimeExtended ermittelt einfach den lokalen timezone offset wie folgt:

// millis sind die gegebenen Millisekunden

long diff = -1000*60*(new Date(millis)).getTimezoneOffset();

// getTimezoneOffset() liefert Minuten, d.h. muß erst in Millisekunden umgewandelt werden
// neg. Vorzeichen ist notwendig, damit der Wert relativ zu UTC wird (statt zu lokal)

Die Summe aus millis und diff wird dann in serielle Werte umgewandelt.

Verwendung: Konstruktor DateTime(millis,timezone_offset), Konstruktor DateTimeExtended(millis).

DateTime nach java.util.Date (und abgeleitete Klassen)

Ein vorhandenes DateTime arbeitet immer mit lokaler Uhrzeit. Umwandlungen nach java.util.Date werden somit immer in die lokale Zeitzone umgesetzt.

Verwendung: statische Methoden DateTimeExtended.getJavaXXX(datetime)

DateTime nach UNIX millis

Ein vorhandenes DateTime arbeitet immer mit lokaler Uhrzeit. Zur Umwandlung nach UNIX millis wird somit immer die lokale Zeitzone mit einbezogen.

Verwendung: Methode DateTime.getUnixMillis()

 

 

 

Offene bzw. zu klärende bzw. festzulegende Punkte

Wann beginnen wir mit der Datumsrechnung?

Ich habe hier einfach mal den 1.1.0001 als Startpunkt angenommen. Bezüglich HIT ist dies weit hergeholt, aber ich pflege bei sich ständig ändernden Parametern vorausschauend (hier müßte man fast sagen: rückschauend :-) zu programmieren. Die Schaltstelle zwischen julianisch und gregorianisch im Oktober 1582 zu programmieren, hat etwas Aufwand gekostet, wurde aber laut der JUnit Tests erfolgreich bewerkstelligt.

java.util.Date übrigens verwendet das selbe Schema an der "gregorianisch / julianisch"-Grenze. Man braucht nur mal z.B. den 6.10.1582 und den 11.10.1582 via Date() anzulegen und beide als String ausgeben - siehe da, beide liefern jeweils den 15.10.1582. java.util.Date hat nur einen Vorteil: es erlaubt auch Daten vor dem 1.1.0001 - DateTime tut dies nicht.

Ich hätte allerdings kein Problem damit, wenn wir uns beispielsweise auf den 1.1.1600 als Startpunkt festlegen. Dann kann der ganze julianische Kalender aus der Klasse herausfallen und die Berechnung sogar etwas schneller von statten gehen. Daten vor dem 1.1.1600 wären aber dann nicht möglich.

Jahreszahlen

Soll sich die Klasse selbst um die Korrektur der Jahreszahlen kümmern, oder eher nicht? Derzeit nimmt DateTime ein Jahr 90 als 90 auf und nicht als 1990. Die Regel, wenn das Jahr >= 70 und < 100 ist, daß automatisch 1900 addiert wird, sollte eher eine externe (statische) Methode übernehmen.

Umwandlung millis in DateTime

In einem Java-System sind die Millis die Millisekunden seit dem 1.1.1970 00:00 Uhr UTC. DateTime muß jedoch bei der Umwandlung auf das zeitzonen-lose DateTime irgendwie die Differenz zwischen UTC und "unserer" Zeit kennen.

DateTimeExtended löst das Problem, indem es vom lokalen System die Zeitzone ermittelt und sie seiner übergeordneten Klasse  DateTime mitgibt.

Auch kein Problem bereiten Umsetzungen von java.util.Date und dessen Subclasses, da diese sowohl die Millisekunden als auch die Zeitzonenoffsets beinhalten. Das kann DateTimeExtended problemlos ermitteln und übernehmen. Die Dates können dann irgeneines Ursprungs sein, entweder lokal generiert oder aus einer DB.

Was ist aber mit den Millisekunden, die in eine DB sollen? Strenggenommen müßte man für Zeitzonen der Timestamps, die für eine Datenbank gedacht sind, über eine Dummy-Abfrage und dem erhaltenen java.sql.Timestamp den Wert des timezone offset ermitteln und diesen dann zusätzlich zu millis als zweiten Parameter dem DateTime mitgeben. Die Abfrage müßte irgendwie regelmäßig jede Stunde geschehen (einen Sommerzeitwechsel bekäme man dann so nämlich nie mit, wenn der Server theoretisch ewig läuft) und das ginge auf die Performance. Wie wird dies am besten gelöst? Der Klasse DateTimeExtended eine java.sql.Connection mitzugeben halte ich jedoch für fehl am Platze.

Aufteilung Time, Date und Timestamp

DateTime kommt derzeit mit den drei Elementen Zeit, Datum und Timestamp (wie z.B. bei java.sql.*) gleichzeitig klar. Problematisch sind beispielsweise reine Zeitangaben, denn: welches Datum wird hierfür verwendet? Oder was passiert, wenn einer Zeit 10:00:00 12 Stunden abgezogen werden? Welchen Tag hat man dann bzw. verwendet man als Basis?

Ich würde daher eventuell eiskalt hergehen und die drei Basiselemente Datum, Zeit und die Mikrosekunden in separate Klassen bzw. Werte auslagern. Ein Timestamp bestünde dann aus Attributen der Objekte Datum und Zeit und einem long mit den Mikrosekunden. Entsprechende Formatier- oder Parser-Funktionen der vorhandenen SimpleDTS-Klasse könnten dann unverändert über die üblichen Konstruktoren ein Timestamp liefern.

Eine Datumsangabe hat dann auch wirklich nur Datumsangaben und keinerlei Zeitkomponenten. Gleiches gilt für die Zeit, die dann ohne Datum auskommt und dann sogar die Möglichkeit hätte, negative Zeitangaben zu besitzen (um das Beispiel vom vorherigen Absatz durchführen zu können).

Sommerzeit

Das Ignorieren der Zeitzonen ist kein Problem. Schwieriger ist es da mit der Sommerzeit. Strenggenommen gibt es beispielsweise kein 27.3.2005 2:30 Uhr. An dem Tag wären 2:00 und 3:00 die selbe Uhrzeit. Nach 02:00:00 käme sofort 03:00:01. DateTime berücksichtigt dies jedoch nicht und betrachtet 2:00 und 3:00 mit einer Stunde Differenz. Die Umrechnung in UNIX-Millisekunden wird jedoch über den Umweg der Klasse Calendar korrekt behandelt, da diese sowohl Zeitzonen als auch die DST-Informationen berücksichtigt. Ich weiß nicht, inwiefern das Ignorieren der Sommerzeit Probleme bereiten könnte. Sollte es sie, wie müßte das dann gehandhabt werden?!?