2017-06-28 36 views
9

googled na chwilę, a najczęściej stosowaną metodą wydaje się byćJak przekonwertować util.Date do time.LocalDate poprawnie dla dat przed 1893

date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 

Jednak metoda ta wydaje się nie do dat przed 1893 -04-01

Poniższy test nie powiedzie się na moim komputerze z wynikami 31.03.1893 zamiast 1.04.1893:

@Test 
public void testBeforeApril1893() throws ParseException { 
    Date date = new SimpleDateFormat("yyyy-MM-dd").parse("1893-04-01"); 

    System.out.println(date); 

    LocalDate localDate2 = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 

    System.out.println(localDate2); 

    assertEquals(1893, localDate2.getYear()); 
    assertEquals(4, localDate2.getMonth().getValue()); 
    assertEquals(1, localDate2.getDayOfMonth()); 
} 

The System.out.prinln S są dla mnie do podwójnego sprawdzenia utworzone daty. Widzę następujący wynik:

Sun Apr 02 00:00:00 CET 1893 
1893-04-02 
Sat Apr 01 00:00:00 CET 1893 
1893-03-31 

Za 1400-04-01 Otrzymuję nawet wynik 1400-04-09.

Czy istnieje metoda konwersji dat przed 1893-04 poprawnie na LocalDate?

Jak niektórzy słusznie zauważyli, przyczynę tej zmiany wyjaśniono w this question. Jednak nie widzę, jak mogę wywnioskować prawidłową konwersję na podstawie tej wiedzy.

+2

Czy możesz podać dane wyjściowe pliku 'System.out.println (date.getTime())'? –

+2

A także 'System.out.println (TimeZone.getDefault(). ToZoneId());' –

+2

Pamiętaj, że żądana konwersja z 'Date' na' LocalDate' zależy od strefy czasowej. Jeśli masz pewność, że obiekt 'Date' miał zostać zinterpretowany w domyślnej strefie czasowej maszyny JVM oraz w (powszednim) kalendarzu gregoriańskim, twoja metoda konwersji powinna być poprawna. –

Odpowiedz

6

Jeśli dopiero parsowania wejście String, to straighforward:

LocalDate d1 = LocalDate.parse("1893-04-01"); 
System.out.println(d1); // 1893-04-01 
LocalDate d2 = LocalDate.parse("1400-04-01"); 
System.out.println(d2); // 1400-04-01 

Wyjście jest:

1893-04-01
1400-04-01


Ale jeśli masz java.util.Date obiekt i trzeba go przekonwertować, to trochę bardziej skomplikowane.

A java.util.Date contains the number of milliseconds from unix epoch (1970-01-01T00:00Z). Możesz więc powiedzieć "jest w UTC", ale kiedy drukujesz, wartość jest "konwertowana" na domyślną strefę czasową systemu (w twoim przypadku jest to CET). I SimpleDateFormat używa również wewnętrznej strefy czasowej wewnętrznie (w sposób niejasny muszę przyznać, że nie w pełni rozumiem).

W powyższym przykładzie wartość millis wynosząca -2422054800000 jest równoważna wartości czasu UTC 1893-03-31T23:00:00Z. Sprawdzanie to wartość w Europe/Berlin czasowej:

System.out.println(Instant.ofEpochMilli(-2422054800000L).atZone(ZoneId.of("Europe/Berlin"))); 

wyjście jest:

1893-03-31T23: 53: 28 + 00: 53: 28 [Europa/Berlin]

Tak, jest to bardzo dziwne, ale wszystkie miejsca używały dziwnych przesunięć przed rokiem 1900 - każde miasto miało swój lokalny czas, zanim nastąpiła norma UTC. To wyjaśnia, dlaczego otrzymujesz 1893-03-31. Obiekt Date drukuje prawdopodobnie dlatego, że stary interfejs API (java.util.TimeZone) nie ma wszystkich historii przesunięć, więc zakłada, że ​​jest to +01:00.

Alternatywą do tej pracy jest, aby zawsze używać UTC jako strefy czasowej:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); 
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // set UTC to the format 
Date date = sdf.parse("1893-04-01"); 
LocalDate d = date.toInstant().atZone(ZoneOffset.UTC).toLocalDate(); 
System.out.println(d); // 1893-04-01 

Pozwoli to uzyskać prawidłową datę lokalne: 1893-04-01.


Ale dla dat przed 1582-10-15, powyższy kod nie działa. To data wprowadzenia kalendarza gregoriańskiego. Wcześniej używano kalendarza juliańskiego i datowano przed nim: need an adjustment.

Mogłem to zrobić z ThreeTen Extra project (rozszerzenie java.time klas, stworzonych przez tego samego faceta BTW). W pakiecie org.threeten.extra.chrono istnieją klasy JulianChronology i JulianDate:

// using the same SimpleDateFormat as above (with UTC set) 
date = sdf.parse("1400-04-01"); 
// get julian date from date 
JulianDate julianDate = JulianChronology.INSTANCE.date(date.toInstant().atZone(ZoneOffset.UTC)); 
System.out.println(julianDate); // Julian AD 1400-04-01 

wyjście będzie:

Julian AD 1400-04-01

Teraz musimy przekształcić JulianDate do LocalDate. Jeśli wykonam LocalDate.from(julianDate), konwertuje się do kalendarza gregoriańskiego (a wynikiem jest 1400-04-10).

Ale jeśli chcesz stworzyć LocalDate z dokładnie 1400-04-01, musisz to zrobić:

LocalDate converted = LocalDate.of(julianDate.get(ChronoField.YEAR_OF_ERA), 
            julianDate.get(ChronoField.MONTH_OF_YEAR), 
            julianDate.get(ChronoField.DAY_OF_MONTH)); 
System.out.println(converted); // 1400-04-01 

wyjście będzie:

1400-04-01

Należy pamiętać, że daty przed 1582-10-15 mają tę regulację i SimpleDateFormat nie może obsłużyć tych przypadków p roperly. Jeśli potrzebujesz pracować tylko z wartościami 1400-04-01 (rok/miesiąc/dzień), użyj LocalDate. Ale jeśli chcesz przekonwertować go na java.util.Date, pamiętaj, że może to nie być ta sama data (z powodu korekt Gregoriańskiego/Juliańskiego).


Jeśli nie chcesz dodawać kolejnej zależności, możesz również wykonywać wszystkie czynności matematyczne ręcznie. Mam dostosowany kod z ThreeTen, ale IMO idealnym jest użycie samego API (co może obejmować przypadki rogu i inne rzeczy jestem prawdopodobnie brakuje po prostu kopiując kawałek kodu):

// auxiliary method 
public LocalDate ofYearDay(int prolepticYear, int dayOfYear) { 
    boolean leap = (prolepticYear % 4) == 0; 
    if (dayOfYear == 366 && leap == false) { 
     throw new DateTimeException("Invalid date 'DayOfYear 366' as '" + prolepticYear + "' is not a leap year"); 
    } 
    Month moy = Month.of((dayOfYear - 1)/31 + 1); 
    int monthEnd = moy.firstDayOfYear(leap) + moy.length(leap) - 1; 
    if (dayOfYear > monthEnd) { 
     moy = moy.plus(1); 
    } 
    int dom = dayOfYear - moy.firstDayOfYear(leap) + 1; 
    return LocalDate.of(prolepticYear, moy.getValue(), dom); 
} 

// sdf with UTC set, as above 
Date date = sdf.parse("1400-04-01"); 
ZonedDateTime z = date.toInstant().atZone(ZoneOffset.UTC); 

LocalDate d; 
// difference between the ISO and Julian epoch day count 
long julianToIso = 719164; 
int daysPerCicle = (365 * 4) + 1; 
long julianEpochDay = z.toLocalDate().toEpochDay() + julianToIso; 
long cycle = Math.floorDiv(julianEpochDay, daysPerCicle); 
long daysInCycle = Math.floorMod(julianEpochDay, daysPerCicle); 
if (daysInCycle == daysPerCicle - 1) { 
    int year = (int) ((cycle * 4 + 3) + 1); 
    d = ofYearDay(year, 366); 
} else { 
    int year = (int) ((cycle * 4 + daysInCycle/365) + 1); 
    int doy = (int) ((daysInCycle % 365) + 1); 
    d = ofYearDay(year, doy); 
} 
System.out.println(d); // 1400-04-01 

wyjście będzie:

1400-04-01

Wystarczy przypomnieć, że cała ta matematyka nie jest potrzebna dla dat po 1582-10-15.


W każdym razie, jeśli masz wejście String i chcesz go analizować, nie używaj SimpleDateFormat - można użyć LocalDate.parse() zamiast. Lub LocalDate.of(year, month, day), jeśli znasz już wartości.

Konwersja tych lokalnych dat z/na java.util.Date jest bardziej skomplikowana, ponieważ Date reprezentuje pełną sygnaturę czasową millis, a daty mogą się różnić w zależności od używanego systemu kalendarza.

+1

Właściwie nie próbuję analizować danych wejściowych String, ale konwertować istniejącą datę na LocalDate. Użyłem tylko String SimpleDateFormat do utworzenia Data dla testu –

+0

@HengruiJiang Wyjaśniam również, jak go przekonwertować, używając 'JulianChronology'. –

+1

Czy istnieje rozwiązanie, które nie zawiera dodatkowych zależności? Naprawdę wolałbym nie dodawać żadnych zależności ... –

3

wydaje się być znany błąd, który nie będzie się Naprawiono: https://bugs.openjdk.java.net/browse/JDK-8061577

Po wielu badaniach zrezygnowałem z każdą prostą metodą API i po prostu przekształcić go ręcznie. Możesz zawinąć datę w sql.Date i zadzwonić pod numer toLocalDate() lub po prostu użyć tych samych przestarzałych metod jak sql.Date. Bez przestarzałych metod, które trzeba konwertować util.Date do kalendarza i uzyskać pola jedno po drugim:

Calendar calendar = Calendar.getInstance(); 
    calendar.setTime(value); 
    return LocalDate.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, 
         calendar.get(Calendar.DAY_OF_MONTH)); 

Jeśli dodatkowych wojsk chcesz mieć dwucyfrowy rok konwersji jak w SimpleDateFormat (przekonwertować datę w przedziale teraz - 80 lat do tej pory + 19 lat) można użyć tej realizacji:

Calendar calendar = Calendar.getInstance(); 
    calendar.setTime(value); 
    int year = calendar.get(Calendar.YEAR); 
    if (year <= 99) { 
     LocalDate pivotLocalDate = LocalDate.now().minusYears(80); 
     int pivotYearOfCentury = pivotLocalDate.getYear() % 100; 
     int pivotCentury = pivotLocalDate.minusYears(pivotYearOfCentury).getYear(); 
     if (year < pivotYearOfCentury) { 
      year += 100; 
     } 
     year += pivotCentury; 
    } 
    return LocalDate.of(year, calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH)); 

Wniosek: jest to naprawdę brzydki i nie mogę uwierzyć, że nie ma żadnego prostego API!

+0

To rozwiązanie wystarczyłoby w moim przypadku, ale przyjąłem odpowiedź Hugosa, ponieważ dostarczył dokładniejszy wgląd –