Date vs LocalDate/LocalDateTime

MiMa

Top Contributor
In meiner Basis Methode für Datums habe ich bisher immer mit SimpleDateFormat und Date gearbeitet.
In Date kann dann sowohl ein Datum "dd.MM.yyyy" als auch ein Datum mit Zeit "dd.MM.yyy HH:mm:ss" enthalten sein.
Date verarbeitet dann auch ein Datum (4.10.2022) bei der Vorlage von "dd.MM.yyyy"

Irgendwo habe ich dann mal gelesen, das dieses Format veraltet sei und mit LocalDate direkt erkennen kann das es sich nur
um ein Datum Handelt oder bei LocalDateTime um ein Datum mit Zeitangabe.

Jetzt habe ich Stunden damit verbracht meine Methode von Date nach LocalDate und LocalDateTime mit den Formatvorlagen
DateTimeFormatter um zu bauen.

Beim Testen ist mir aufgefallen dass es hier Probleme gibt bei (4.10.2022) mit dem Format "dd.MM.yyyy"?!?
Der letzte Ausweg wäre dann die führende null dran zu basteln oder habe ich da etwas übersehen?

Meine Methode soll jedes erdenkliche Datum als String erkennen und in ein Local Date oder LocalDateTime umwandeln können.
Oder sollte ich doch bei dem Date und SimpleDateFormat bleiben?
 
Hier klinke ich mich mal ein um auf dem Laufenden zu bleiben. Etwas ähnliches hatte ich vorhin auch mal vor der Nase 🙂
Wenn ich das richtig geschnallt habe braucht es doch den Formatter und dann kann man es - theoretisch - parsen?! Glaub ich..
 
Oder sollte ich doch bei dem Date und SimpleDateFormat bleiben?
Nein.

Irgendwo habe ich dann mal gelesen, das dieses Format veraltet sei
Das Format tt.mm.jjjj dürfte das in Deutschland immer noch vorherrschende Format sein. Tatsächlich wurde in der DIN 5008 eine Zeit lang nur das internationale Format akzeptiert, davon ist man aber wieder abgerückt, auch wenn das internationale Format weiterhin empfohlen wird.

Ein ausschließlich numerisch angegebenes Datum muss nach DIN 5008 in der Form tt.mm.jjjj inkl. führender Nullen angegeben werden. Das Jahr ist immer vierstellig anzugeben. Irgendwas wie "1.1.22", "1.01.2022" usw. entspricht somit nicht der Norm. Bei Schreibweisen, die Text verwenden, darf dagegen beim Tag keine führende Null angegeben werden: "1. Januar 2022" wäre korrekt, "01. Januar 2022" nicht, ebenso wenig wie "1. Januar 22".

Beim Testen ist mir aufgefallen dass es hier Probleme gibt bei (4.10.2022) mit dem Format "dd.MM.yyyy"?!?
Der letzte Ausweg wäre dann die führende null dran zu basteln oder habe ich da etwas übersehen?
DateTimeFormatter.ofPattern("d.M.yyyy") parst "09.05.2022", "9.05.2022", "09.5.2022" und "9.5.2022". Das Jahr muss hier zwangsweise vierstellig angegeben werden. Will man das nicht, kann man "d.M.y" verwenden, dann wird aus einem "22" allerdings das Jahr "0020".

SimpleDateFormat parst eine abgekürzte Jahresangabe etwas anders: "SimpleDateFormat must interpret the abbreviated year relative to some century. It does this by adjusting dates to be within 80 years before and 20 years after the time the SimpleDateFormat instance is created", wie man der Doku entnehmen kann.

Wenn man dieses Verhalten und sowohl zwei- als auch vierstellige Jahresangaben zulassen möchte:
Java:
DateTimeFormatter format = new DateTimeFormatterBuilder()
        .appendPattern("d.M.")
        .appendValueReduced(ChronoField.YEAR, 2, 4, Year.now().getValue() - 80)
        .toFormatter();

Für die Ungläubigen:
Java:
public class Test {
    static final DateTimeFormatter format = new DateTimeFormatterBuilder()
            .appendPattern("d.M.")
            .appendValueReduced(ChronoField.YEAR, 2, 4, Year.now().getValue() - 80)
            .toFormatter();

    public static void main(String[] args) {    
        LocalDate expected = LocalDate.of(2022, 9, 5);
        assertEquals(expected, LocalDate.parse("5.9.22", format));
        assertEquals(expected, LocalDate.parse("05.9.22", format));
        assertEquals(expected, LocalDate.parse("5.09.22", format));
        assertEquals(expected, LocalDate.parse("05.09.22", format));
        assertEquals(expected, LocalDate.parse("5.9.2022", format));
        assertEquals(expected, LocalDate.parse("05.9.2022", format));
        assertEquals(expected, LocalDate.parse("5.09.2022", format));
        assertEquals(expected, LocalDate.parse("05.09.2022", format));
        System.out.println("Tests passed");
    }

    private static void assertEquals(Object a, Object b) {
        if (!a.equals(b)) {
            throw new RuntimeException("Expected " + a + " but got " + b);
        }
    }
}
 
Danke für die ausführliche Beschreibung.
Ich Programmiere gerne und habe schon einiges geschrieben, aber leider kenne ich oft nur die Hauptbefehle und die vielen Unterfunktionen eines Befehles wie Reduced.., ChronieField.,... usw leider zu 97% nicht. Halt nur das was man aus Büchern, Udemy und Youtube so lernt.
Alle haben leider eines gemeinsam, alle verarbeiten den gleichen Stoff in der gleichen weise manchmal etwas abweichend.

Um all diese String-Datums mit einem Formatter umformen zu können inklusive Echtheitsprüfung mit und ohne Zeit, ungültige aussortiert, habe ich in 350 Zeilen Stringverarbeitungen gemacht um es normgerecht hin zu basteln um es mit Date und SimpleDateFormat konvertieren zu können.
Mit meinem Wissen halt sehr viel mehr Code.

Wäre ich nicht mit dem Datumsformat auf LocalDate/Time umgestiegen, hätte ich von all dem nichts gewusst.
Ich fand das ja schon so toll, das ich aus einem Text alle möglichen Datumsangaben mit Regex finden konnte.

@mihe7
Die Test Klasse, die du da oben gepostet hast, ist das ein Test mit JUnit?
Hab noch nie JUnit benutzt und auch noch nie einen Test gesehen.
Ist der Test die assertEquals Methode oder das in der Main?

Danke
 
Die Test Klasse, die du da oben gepostet hast, ist das ein Test mit JUnit?
Nein aber ich habe den Code daran angelehnt 🙂 In JUnit sähe das etwa so aus:

Java:
public class FormatTest {
    static final DateTimeFormatter format = new DateTimeFormatterBuilder()
            .appendPattern("d.M.")
            .appendValueReduced(ChronoField.YEAR, 2, 4, Year.now().getValue() - 80)
            .toFormatter();

    @Test
    public void should_parse() {
        LocalDate expected = LocalDate.of(2022, 9, 5);
        assertEquals(expected, LocalDate.parse("5.9.22", format));
        assertEquals(expected, LocalDate.parse("05.9.22", format));
        assertEquals(expected, LocalDate.parse("5.09.22", format));
        assertEquals(expected, LocalDate.parse("05.09.22", format));
        assertEquals(expected, LocalDate.parse("5.9.2022", format));
        assertEquals(expected, LocalDate.parse("05.9.2022", format));
        assertEquals(expected, LocalDate.parse("5.09.2022", format));
        assertEquals(expected, LocalDate.parse("05.09.2022", format));
    }
}

bzw. dort lassen sich Tests auch parametrisieren:
Java:
public class FormatTest {
    static final DateTimeFormatter format = new DateTimeFormatterBuilder()
            .appendPattern("d.M.")
            .appendValueReduced(ChronoField.YEAR, 2, 4, Year.now().getValue() - 80)
            .toFormatter();

    @ParameterizedTest
    @ValueSource(strings={
            "5.9.22", "05.9.22", "5.09.22", "05.09.22", "5.9.2022", "05.9.2022", "5.09.2022", "05.09.2022"
    })
    public void should_parse(String date) {
        LocalDate expected = LocalDate.of(2022, 9, 5);
        assertEquals(expected, LocalDate.parse(date, format));
    }
}

Ist der Test die assertEquals Methode oder das in der Main?
Der Test ist die main-Methode (vgl. mit der JUnit-Variante). Dort findest Du mehrere Testfälle der Form «wenn ich "5.9.22" mit Hilfe von format parse, dann erwarte ich ein LocalDate, das gleich einem LocalDate.of(2022, 9, 5) ist» und die assertEquals-Methode dient dazu, einen Testfall zu spezifizieren und automatisch zu überprüfen.

Ich fand das ja schon so toll, das ich aus einem Text alle möglichen Datumsangaben mit Regex finden konnte.
Das kannst/musst Du auch weiterhin tun, wenn Du alternative Formate berücksichtigen willst. Der gezeigte Formatter erschlägt zwar die obigen Fälle aber z. B. nicht "5. September 2022".

habe ich in 350 Zeilen Stringverarbeitungen gemacht um es normgerecht hin zu basteln um es mit Date und SimpleDateFormat konvertieren zu können.
Ist doch ok, da lernt man was dabei.

aber leider kenne ich oft nur die Hauptbefehle und die vielen Unterfunktionen eines Befehles wie Reduced.., ChronieField.,... usw leider zu 97% nicht.
Das sind Klassen und Methoden und tröste Dich: ich kenne auch viele Dinge nicht bzw. habe ich wieder vergessen. Aber: die Pakete sind strukturiert (java.time - was wird man da wohl finden?), so dass man schon einmal ungefähr weiß, wos man in der Dokumentation suchen muss. Und was man ständig benötigt, weiß man sowieso auswendig. Wenn Du eine IDE verwendest und dort nach Date die Vervollständigung verwendest, bekommst Du eben Vorschläge, die man dann nachschauen kann (die Doku gibts normalerweise ja auch gleich in der IDE dazu).
 
Vielen Dank das war sehr aufschlussreich.

Eine letzte Frage zum Datum habe ich noch!
Kann die Umwandlung in Millisekunden nur in LocalDateTime erfolgen?

Bei Date ging es zwar aber ich kann mir vorstellen, das es eine Differenz gibt, bei einem Datum mit Zeit und selbiges Datum ohne Zeit.
Eine Lösung könnte ich mir Vorstellen ein LocalDate in ein LocalDateTime um zu wandeln und eine fixe Zeit mitgeben, wie 12 Uhr Mittags oder so.

Wie wird das denn sonst so gehandhabt?
Zum vergleichen von Datums werden doch bestimmt nicht die Millisekunden heran gezogen, wenn es before und after gibt?

Danke
 
Kann die Umwandlung in Millisekunden nur in LocalDateTime erfolgen?
Sagen wir besser andersrum: in LocalDate erfolgt sie nicht.

Bei Date ging es zwar aber ich kann mir vorstellen, das es eine Differenz gibt, bei einem Datum mit Zeit und selbiges Datum ohne Zeit.
Eine Lösung könnte ich mir Vorstellen ein LocalDate in ein LocalDateTime um zu wandeln und eine fixe Zeit mitgeben, wie 12 Uhr Mittags oder so.
Du hast Dir Deine Fragen gerade selbst beantwortet 🙂

Eine java.util.Date ist am ehesten mit einem java.time.Instant vergleichbar. Ein LocalDate kennzeichnet dagegen einen Kalendertag. Mit Millisekunden kann man eine Dauer darstellen, für einen Zeitpunkt braucht man zusätzlich noch einen Bezugspunkt, z. B. Millisekunden seit 01.01.1970T00:00:00.0Z oder Millisekunden sein 00:00:00.0 Uhr des heutigen Tages usw.

Zum vergleichen von Datums werden doch bestimmt nicht die Millisekunden heran gezogen, wenn es before und after gibt?
Wozu auch?
 
Ich habe meine Datumsklasse jetzt komplett von Date und SimpleDateFormat auf LocalDate, LocalDateTime und DateTimeFormatter umgestellt.
Alle Datumstest waren bis auf eine erfolgreich.

Das Datum 30.02.2022 wurde nach dem Parsen als gültig deklariert und als 28.02.2022 zurück gegeben.
Das mag vielleicht aus einer bestimmten sich korrekt sein, da der letzte Tag im Februar dem zurückgegebenen entspricht.

Ich bin gerade in der Überlegung ob ich das Ergebnis so verarbeiten sollte, oder doch als ungültiges Datum akzeptieren?
Alle ungültigen Datumsangaben sollen in meinem Programm einfach ignoriert werden.

Vorher hatte ich mit "setLenient(false)" das Strikte Parsen definiert und da wurde der 30.02.2022 als ungültig deklariert.
Mit LocalDate/LocalDateTime steht diese Möglichkeit nicht mehr zur Verfügung.
Ich hatte auch schon mal in den Unterfunktionen nachgesehen, aber ich konnte jetzt nicht wirklich was finden.

Sollte ich für diese Situation konsequent durchgreifen und als ungültig ansehen, oder das neue Ergebnis als 28.02.2022 annehmen?
 
Zuletzt bearbeitet:
Ist die Reaktion des Parsens denn so beabsichtigt? 30.02.2022 => 28.02.2022.
Wie könnte ich das umgehen?
Am liebsten würde ich es als ungültig erklären.
 
Ist die Reaktion des Parsens denn so beabsichtigt? 30.02.2022 => 28.02.2022.
Wie könnte ich das umgehen?
Am liebsten würde ich es als ungültig erklären.
Ja, das ist so erwünscht (per Standard) und kann auch umgesetzt werden. Das wurde doch auch schon hier behandelt:

Das ist auch hier 1:1 anzuwenden. Du schaust Dir LocalDate an und siehst da dann die parse Methode, die auch ein DateTimeFormatter mit bekommt.
Und wenn man dann DateTimeFormatter anschaut, dann kann man dort nach "lenient" schauen - das war ja aus deinem anderen Thread der verwendete Fachbegriff:
The ResolverStyle is an enum that offers three different approaches, strict, smart and lenient. The smart option is the default. It can be set using withResolverStyle(ResolverStyle).
Und das führt dann auch direkt zu https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/format/ResolverStyle.html - und dort findet sich dann das STRICT, das Du willst.

Edit: Das nur, um Dir den Weg zur Selbsthilfe etwas aufzuzeigen. Die Schilderung des Weges soll also kein Vorwurf oder so sein, dass Du es nicht selbst so recherchiert hast. Wir helfen gerne nur eben zeige ich auch gerne den Weg mit auf.
 
Ich habe auch schon einiges nachgeschlagen, konnte es aber unter "Lenient(false)" unter der neuen API nichts finden.
Hätte nicht gedacht das es unter ResolveStyle und Strict zu finden wäre.
In der time API ist halt einiges anders.

aktuell habe ich den Format-Parameter wie folgt geschrieben
Code:
format.withResolverStyle(ResolverStyle.STRICT)
Jetzt ist der 30.02.2022 ungültig, der 06.12.2022 auch, sollte aber nicht ungültig sein?

Code:
format.withResolverStyle(ResolverStyle.LENIENT)
Ist der 30.02.2022 wieder gültig??

Auszug aus dem Log
Code:
Parse 'LocalDate' Format
Das Datum ist 'gültig' 2022-03-02 true
Datum - datum (unformatiert)  : 2022-03-02
datum               : 30.02.2022
datumDatei      : 2022-03-02
tag                    : 30
monat               : 02
jahr                   : 2022


Anscheinend habe ich das codieren dieser Optionen noch nicht richtig verstanden?!?!
Das Prinzip jedoch ist mir klar.
 
Java:
public class Test {
    static final DateTimeFormatter format = new DateTimeFormatterBuilder()
            .appendPattern("d.M.")
            .appendValueReduced(ChronoField.YEAR, 2, 4, Year.now().getValue() - 80)
            .toFormatter();

    static String[] dates = {
        "5.9.22", "05.9.22", "5.09.22", "05.09.22", "5.9.2022", "05.9.2022", "5.09.2022", "05.09.2022"
    };

    static String[] lenientDates = {
        "29.2.22", "30.2.22"
    };

    static LocalDate[] smartExpected =  {
        LocalDate.of(2022, 2, 28), LocalDate.of(2022, 2, 28)
    };

    static LocalDate[] lenientExpected =  {
        LocalDate.of(2022, 3, 1), LocalDate.of(2022, 3, 2)
    };

    public static void main(String[] args) {
        DateTimeFormatter lenient = format.withResolverStyle(ResolverStyle.LENIENT);
        DateTimeFormatter strict = format.withResolverStyle(ResolverStyle.STRICT);

        LocalDate expected = LocalDate.of(2022, 9, 5);
        for (String date : dates) {
            assertEquals(expected, LocalDate.parse(date, format));
            assertEquals(expected, LocalDate.parse(date, lenient));
            assertEquals(expected, LocalDate.parse(date, strict));
        }

        for (int i = 0; i < lenientDates.length; i++) {
            assertEquals(smartExpected[i], LocalDate.parse(lenientDates[i], format));
            assertEquals(lenientExpected[i], LocalDate.parse(lenientDates[i], lenient));
            try {
                LocalDate.parse(lenientDates[i], strict);
                throw new RuntimeException("Expected a DateTimeParseException but got none.");
            } catch (DateTimeParseException ex) {
                // expected
            }
        }
        System.out.println("Tests passed");
    }

    private static void assertEquals(Object a, Object b) {
        if (!a.equals(b)) {
            throw new RuntimeException("Expected " + a + " but got " + b);
        }
    }
}
 

Zurück
Oben