Wyrażenia regularne i walidacja dat

0

Pracuję nad następującym zadaniem:
Daty w pliku tekstowym o nazwie{user.home}/test/daty.txt maja postać YYYY-MM-DD (rok- miesiąc- dzień). Oprócz dat, plik zawiera inne informacje. Wczytać plik i wypisać w jednym wierszu wszystkie poprawne daty - czyli: po wyróżnieniu wszystkich fragmentów, które mogą być datami sprawdzić ich poprawność (np. czy właściwa jest liczba dni w miesiącu)

Program powinien w specyficzny sposób traktować niejednoznaczną informację (np. 2009-01-111 - czy to błędna data czy potraktować ją jako 2009-01-11).
Sposób traktowania takich sytuacji wyjaśnia poniższy przykład.

Dane w pliku:

2007-01-12Jakis txt2008-01-31 xxx 2008-02-29 2008-15-10 2008-19-45 2009-05-01
20999-11-11 pppp 2001-00-01 09-01-01 2001-01-00 2009-01-111 2009-02-29 1998-11-11

Program powinien wyprowadzić:
2007-01-12
2008-01-31
2008-02-29
2009-05-01
1998-11-11

Zalecenia: użyć skanera, wyrażeń regularnych (ale same wyrażenia reg. nie wystarczą).

Póki co usunąłem pojawianie się błędnych dat poza jedną (pogrubioną wyżej) i uzyskuję taki zapis:
2007-01-12
2008-01-31
2008-02-29
2009-05-01
2009-02-29
1998-11-11

Nie wiem jak sprawić, by mój kod odsiewał niewłaściwe daty jak 2009-02-29, czy inne niezgodne z kalendarzem.

Póki co użyłem matchera by uzyskać właściwe daty, oraz za pomocą właściwego regex'a usunąłem inne zbędne dla wzoru odpowiedzi daty.

1

A możesz dodatkowo sprawdzać datę otrzymaną w wyniku wyrażenia regularnego? Wystarczyłaby funkcja, która dla lutego sprawdzi, czy rok jest przestępny, a dla innych miesięcy, czy dzień mieści się w określonych przedziałach. Jeśli, któryś warunek nie jest spełniony, to odrzucamy taką datę.

1

Kiedy liczba dni w lutym wynosi 29, powinna być sprawdzana reszta z dzielenia danego roku przez 4. Jeśli wynosi 0 - jest ok, a jeśli nie to odrzucasz.

Powyższy algorytm byłby niewystarczający dla lat wcześniejszych niż 1901 i późniejszych niż 2099. Wtedy musiałbyś też sprawdzać podzielność przez 100 i podzielność przez 400. Natomiast z Twojego przykładu zakładam, że nie musisz się tym przejmować.

1

Może po prostu spróbuj stworzyć obiekt Date na podstawie dopasowanego łańcucha?
https://www.javatpoint.com/java-string-to-date

0

Dzięki za te pomysły i szybkie odp.

Sprawdzam u siebie lata od 1000 do 9999, bo wykładowca lubi wrzucać dziwne przykłady. Póki co ubezpieczyłem się, by warianty trzy cyfrowe nie miały prawa się pojawić jako data. Jak również dziwne formaty jak 0009, czy 0999 itd. @C.mind, mógłbyś rozwinąć ten pomysł? Może to jest to czego szukam.

@elwis próbuję od rana obiektu Date, ale mam problem póki co z nim i ze zrozumieniem jak to połączyć z tym co dotychczas zrobiłem (poprzez matchera za regexem). Teraz próbuję dostosować info z tego adresu https://www.baeldung.com/java-string-valid-date pod moje potrzeby (formatter ze stringa mojego)

1
Madrag napisał(a):

@C.mind, mógłbyś rozwinąć ten pomysł? Może to jest to czego szukam.

W tym, co tutaj napiszę założę, że już masz porobione integery z dnia, miesiąca i roku.

int day = 29;
int month = 2;
int year = 1900;

if(month == 2 && day == 29) {
     if(year%4 == 0 && (year%100 != 0 || year%400 == 0))  {
         // data jest ok 
     } else {
         // odrzuc
     }
}

W ten sposób możesz sprawdzić, czy jeśli w dacie jest 29 luty, to czy dany rok faktycznie był rokiem przestępnym. Na tej podstawie go akceptujesz lub odrzucasz.
Przykładowo rok 1900 zostałby tu odrzucony, bo wprawdzie jest podzielny przez 4, ale jest też podzielny przez 100 i nie jest podzielny przez 400 (takie lata są latami wyjątkowymi i nie są przestępne). Za to rok 2000 byłby zaakceptowany.

0

@C.mind:
If'a rozumiem, że jak pisałeś - sprawdza czy rok jest przestępny, jeśli tak, to nie pozwala dać 29 jako dnia. To samo należy też zrobić w przypadku dni co mają tylko 30 dni dodając else if i podobny warunek na konkretne miesiące? Czy lepiej to zrobić jako oddzielny if, a nie dodawać ifa w ifie?

1
Madrag napisał(a):

@C.mind:
If'a rozumiem, że jak pisałeś - sprawdza czy rok jest przestępny, jeśli tak, to nie pozwala dać 29 jako dnia. To samo należy też zrobić w przypadku dni co mają tylko 30 dni dodając else if i podobny warunek na konkretne miesiące? Czy lepiej to zrobić jako oddzielny if, a nie dodawać ifa w ifie?

Jeśli rok jest przestępny to pozwala zaakceptować 29.02. W normalnych latach nie ma takiej daty.
Nadmierne zagnieżdżanie ifów w ifach to jest antypattern, więc odradzam.

Poczytaj jeszcze o tym, bo może oszczędzi Ci to sporo roboty w postaci pisania miliona warunków:
https://stackoverflow.com/questions/226910/how-to-sanity-check-a-date-in-java/4528094

0
Madrag napisał(a):

@elwis próbuję od rana obiektu Date, ale mam problem póki co z nim i ze zrozumieniem jak to połączyć z tym co dotychczas zrobiłem (poprzez matchera za regexem). Teraz próbuję dostosować info z tego adresu https://www.baeldung.com/java-string-valid-date pod moje potrzeby (formatter ze stringa mojego)

Nie rozumiem w czym rzecz. Pod tym linkiem masz dokładnie to o czym napisałem. Co tu dostosowywać?

0

@elwis: Nie chcę tylko przepisać i wsio, a zrozumieć jak najwięcej możliwości rozwiązania tego problemu.

0

To dobrze. Znacznie ułatwi zadanie konkretnego pytania co nie jest jasne? Albo zamów prywatną lekcję z całym zadaniem i multum różnych możliwości. To chyba fair.

0

@elwis: Problem wygląda tak:

Za pomocą regexa i matchera wyciągnąłem daty z pliku txt. Upewniłem się, by nie było dat 0999-12-02 czy 2009-39-40. Tak samo jak upewniłem się, by nie czytał danych jako daty gdy w txt jest niewłaściwy zapis (2009-03-111). Elementem, dla którego umieściłem ten post jest to, że nie wiem jak mogę zabezpieczyć się przed datami, które mają 30 a nie 31 dni, lub lutym, który ma 28 lub 29 dni. Jednym rozwiązaniem jest to co wskazał C.mind, ale mnie zastanawia, czy mogę jakoś użyć opcji kalendarza by wyniki, które mam z matchera sprawdzić opcjami kalendarza i odfiltrować te, które nie pasują do niego.

Chcę zrozumieć zadanie. Każdy dał mi różne opcje rozwiązania, lub trochę inne które chętnie sprawdzę. Tyle. Łatwiej by mi było zrobić to zadanie bez regexa, przy użyciu calendara i ifów, ale nie po to podjąłem się nauki javy, by być zadowolonym tylko z jednego rozwiązania. .

1

Przeczytaj odpowiedzi w linku, który wcześniej wkleiłem. Jeśli ustawisz setLenient(false);, to przy próbie wczytania błędnej daty będzie Ci rzucało wyjątek.
Nie wiem czy możesz korzystać z tego typu opcji, ale jeśli tak, to to by było bardziej eleganckie rozwiązanie.

1

@C.mind: Tak, patrzyłem, ale dopiero teraz ruszam z tym. To chyba starczy mi do wykonania całego kodu zgodnie z wymaganiami. Dzięki za świetne wskazówki!

0

@Madrag: po pierwsze, dane testowe powinny być jak najbardziej podchwytliwe, więc usuwanie tych o których wspomniałeś to słaby pomysł.

Po drugie ify to nakładanie pracy i wynajdywanie koła na nowo. Prędzej spróbowałbym metody split. Dla prostych przypadków czasem nawet lepsze to niż regex.

Po trzecie, radzę popracować nad szukaniem sedna problemu. Tego co jest istotne. Bardzo przydatna umiejętność, nie tylko w programowaniu, oddzielić sygnał od szumu.
Powodzenia.

0

Dane były podchwytliwe, ale źle zrozumiałeś to co napisałem. Usunąłem niewłaściwe formy poprzez odfiltrowanie ich przy użyciu regexa i matchera. Zgodnie z poleceniem. Przeczytaj moje posty raz jeszcze.

0

Jakby ktoś potrzebowałby odpowiedzi jak zrobić to zadanie:

Wczytanie pliku Scannerem. Do testowania użyłem
String fname= "test"; zamiast System.getProperty() + " ";

Utworzenie stringa zawierającego scanner.nextLine();, który odczytany zostanie przez matcher'a.

Następnie ustawiłem regex by nie przepuszczał takich dat:

  • 2008-15-10
  • 2008-19-45
  • 20999-11-11
  • 2001-00-01
  • 09-01-01
  • 2001-01-00

(?!\d+) - ten zapis przy części regexa odpowiadającego za dni sprawił, że zapis 2009-01-111 nie jest odczytywany jako data 2009-01-11. (co było częścią wymogów zadania).

następnie wrzuciłem matchera z String "line", by odczytać dane ze scannera i odfiltrować plik txt zgodnie z zapisem regexa.

Następnie najłatwiejszy sposób użycia SimpleDateFormat i dostosowaniem wyników regexa było rozbicie wyniku z matchera przez Split i sprawdzenie każdej uzyskanej przez regex daty względem poprawności miesięcy i dni.

Utworzyłem String[] dateSplit = wynik.split("\s+");

dodałem for i końcówkę kodu:
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);

		 //tutaj użycie Date date i zapisanie result z dodaniem wyniku dateSplit. 

i na sam koniec sout z wynikiem.

Osoby tu komentujące dużo mi pomogły w zrozumieniu tego zadania i uzyskaniu działającego kodu. Pewnie znajdą się osoby, które te rady wykorzystałyby skuteczniej a moje rozwiązanie jeszcze bardziej uprościły. Mam nadzieję, że mój post przyda się innym w rozwiązaniu podobnych problemów.

0

A czy możesz wrzucić jak wyglądał Twój regex w całości?

0

@kuba2019:

  String regex = "(?<year>[1-9][0-9][0-9][0-9])"
              + "-?(?<month>1[0-2]|0[1-9])"
              + "-?(?<day>3[01]|0[1-9]|[12][0-9])(?!\\d+)";
0

@Madrag, to ten regexp jest raczej bez szans w starciu z latami przestępnymi, tak samo nie uwzględnia sprawdzenia, czy w danym miesiącu masz 30 czy 31 dni.

Pisane z pamięci:

[1-9]\d((00)|0[^48]|[13579][^26]|[2468][^048])-((0[13578]|10|12)-(0[1-9]|[12]\d|30|31)|(0[469]|11)-(0[1-9]|[12]\d|30)|(02-(0[1-9]|[1]\d|2[123456780])))|[1-9]\d(|0[48]|[13579][26]|[2468][048])-((0[13578]|10|12)-(0[1-9]|[12]\d|30|31)|(0[469]|11)-(0[1-9]|[12]\d|30)|(02-(0[1-9]|[1]\d|2\d)))

Powyższy regexp już uwzględnia lata przestępne oraz różną ilość dni w miesiącu. Przetestować można go tutaj. Da się to jeszcze trochę uprościć, wprowadzając, chociażby grupy nazwane, lookbehind i lookahead, ale…

Po pierwsze nie daje to aż takiego wzrostu czytelności, jak się można spodziewać, bo co prawda zwięźlej wygląda, lecz mało kto ogarnia bardziej złożone elementy wyrażeń regularnych.
Po drugie będzie potwornie spowalniać, i tak już wolne, rozwiązanie.
Po trzecie nie wszystkie silniki wyrażeń regularnych łykają takie konstrukty.

ps. możesz się pobawić różnymi językami i sobie porównać jak działa silnik wyrażeń w JS, a jak w php czy pythonie.

1
Koziołek napisał(a):

@Madrag, to ten regexp jest raczej bez szans w starciu z latami przestępnymi, tak samo nie uwzględnia sprawdzenia, czy w danym miesiącu masz 30 czy 31 dni.

Przecież on nie sprawdzał przestępności i liczby dni regexem, tylko poprzez SimpleDateFormat.

0

Powinni byli dorzucuć do składni wyrażeń regularnych specjalne literały odpowiadające za poszczególne elementy daty...

0

Regex służył tylko wypisaniu formatu daty. Przestępne dni jak i poprawność dni sprawdzałem po rozbiciu stringa Splitem i wrzuceniem wyników z tabeli do dateFormat.

0

Napisałem taki skrypt w Pythonie:

import re
import datetime

def is_date_valid(date_text):
	try:
		datetime.datetime.strptime(date_text, '%Y-%m-%d')
	except ValueError:
		return False
	
	return True

dane_z_pliku = """2007-01-12Jakis txt2008-01-31 xxx 2008-02-29 2008-15-10 2008-19-45 2009-05-01
20999-11-11 pppp 2001-00-01 09-01-01 2001-01-00 2009-01-111 2009-02-29 1998-11-11"""

regexp_result = re.findall("\d+-\d+-\d+", dane_z_pliku)
final_result = []

for date in regexp_result:
	if is_date_valid(date):
		final_result.append(date)

print (final_result)

Można uruchomić tutaj: https://ideone.com/4Ud8uj

Dla podanych danych wejściowych zwraca spodziewane wyniki.
W Javie zamiast strptime chyba to samo zrobisz korzystając z SimpleDateFormat (jeśli rzuca odpowiednim wyjątkiem...): https://stackoverflow.com/a/7512985/1639851

1 użytkowników online, w tym zalogowanych: 0, gości: 1