Klasa Calendar dla opornych
Wpis dla osób mającym mniejsze doświadczenie w javie, ale i bardziej doświadczeni mogą się z tym zetknąć.
Tak, wiem mamy Javę 8 i w ogóle…
Problem jest jednak taki, że czasami musimy pracować z narzędziami napisanymi przed J8 i one uparcie chcą Calendar.
Problem
Przyjrzyjmy się pewnej metodzie
Listing 1. Metoda parsująca nietypowy format daty
public Calendar parseBusinessDate(String date) {
if (date.length() != 7) return null;
String year = date.substring(0, 4);
String month = date.substring(4);
int y = Integer.parseInt(year);
int m = Integer.parseInt(month);
Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, y);
c.set(Calendar.MONTH, m - 1);
c.set(Calendar.DATE, c.getActualMaximum(Calendar.DATE));
return c;
}
Data ma siedem znaków. Cztery pierwsze to rok, a potem trzy znaki miesiąca. Datę ustawiamy na ostatni dzień danego miesiąca. Dlaczego tak nie pytajcie. Teraz napiszmy test. Będzie on specyficzny, bo chcemy sprawdzić rok przestępny:
Listing 2. test dla roku przestępnego
@Test
public void shouldParseLeapYear(){
// when
Calendar result = sut.parseBusinessDate("2012002");
// then
Assert.assertEquals(result.get(Calendar.YEAR), 2012);
Assert.assertEquals(result.get(Calendar.MONTH), Calendar.FEBRUARY);
Assert.assertEquals(result.get(Calendar.DATE), 29);
}
Wygląda ok, prawda? To uruchom ten test 30 lub 31 dnia miesiąca…
Ciekawe prawda?
Przyczyna i rozwiązanie
Problemem jest sposób w jaki inicjowana jest instancja Calendar. Wywołując getInstance otrzymujemy obiekt wskazujący na bieżącą datę. Oznacza to, że dzień może być ustawiony na 30 albo 31. Jednocześnie w czasie wywołania metody parseBusinessDate zanim ustawimy dzień ustawiamy rok i miesiąc. W efekcie jeżeli ustawimy miesiąc, który jest krótszy niż bieżący i zrobimy to w ostatnim dniu miesiąca to nastąpi „przepełnienie” daty i przeskoczymy do kolejnego miesiąca.
Co ciekawe błąd tego typu może umknąć naszej uwadze i testom jeżeli tylko sposób naszej pracy powoduje, że w te nieszczęsne ostatnie dni miesiąca nie puszczamy z jakiś powodów testów np. mamy dwa dni spotkań, jest weekend i nie ma commitów, albo code freeze przed releasem. Jeżeli jeszcze po stronie klienta procedury, które mogłyby wykryć błąd są puszczane pierwszego dnia roboczego, a nie ostatniego to taki babol może długo pozostawać w ukryciu.
Rozwiązanie problemu jest proste. Wystarczy „zresetować” zmienną po utworzeniu na przykład ustawiając dzień na 1 albo czas na zerową milisekundę.
Listing 3. Poprawiona metoda parsująca nietypowy format daty
public Calendar parseBusinessDate(String date) {
if (date.length() != 7) return null;
String year = date.substring(0, 4);
String month = date.substring(4);
int y = Integer.parseInt(year);
int m = Integer.parseInt(month);
Calendar c = Calendar.getInstance();
c.setTimeInMillis(0L);
c.set(Calendar.YEAR, y);
c.set(Calendar.MONTH, m - 1);
c.set(Calendar.DATE, c.getActualMaximum(Calendar.DATE));
return c;
}
Mała rzecz, a potrafi wkurwić.