2017-06-28 1 views
9

je googlé pendant un certain temps et le plus souvent méthode utilisée semble êtreComment convertir util.Date à time.LocalDate correctement pour les dates avant 1893

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

Cependant, cette méthode semble échouer pour les dates avant 1893 -04-01

Le test suivant échoue sur ma machine avec un résultat de 31/03/1893 au lieu de 01/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()); 
} 

Les System.out.prinln s sont pour moi vérifier les dates créées. Je vois la sortie suivante:

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

Pour 01/04/1400 je reçois même une sortie de 09/04/1400.

Existe-t-il une méthode pour convertir correctement les dates antérieures à 1893-04 en LocalDate?

Comme certains ont utilement souligné, la raison de ce décalage est expliquée dans this question. Cependant, je ne vois pas comment je peux déduire une conversion correcte basée sur cette connaissance.

+2

Pouvez-vous fournir la sortie de 'System.out.println (date.getTime())'? –

+2

Et aussi 'System.out.println (TimeZone.getDefault(). ToZoneId());' –

+2

Prenez garde que votre conversion souhaitée de 'Date' à' LocalDate' dépend du fuseau horaire. Si vous êtes sûr que votre objet 'Date' était supposé être interprété dans le fuseau horaire par défaut de votre JVM et dans le calendrier grégorien (proleptique), votre méthode de conversion devrait être correcte. –

Répondre

6

Si vous êtes juste une analyse syntaxique entrée String, il est 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 

La sortie est:

1893-04-01
1400-04-01


Mais si vous avez un java.util.Date objet et besoin de le convertir, c'est un peu plus compliqué.

A java.util.Date contains the number of milliseconds from unix epoch (1970-01-01T00:00Z). Donc, vous pouvez dire "c'est en UTC", mais quand vous l'imprimez, la valeur est "convertie" dans le fuseau horaire par défaut du système (dans votre cas, c'est CET). Et SimpleDateFormat utilise également le fuseau horaire par défaut en interne (de façon obscure que je dois admettre que je ne comprends pas complètement).

Dans votre exemple, la valeur en millisecondes de -2422054800000 est équivalente à l'instant UTC 1893-03-31T23:00:00Z. Si vous cochez cette valeur dans Europe/Berlin fuseau horaire:

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

La sortie est:

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

Oui, c'est très étrange, mais tous les lieux utilisaient des décalages étranges avant 1900 - chaque ville avait son propre heure locale, avant que la norme UTC n'ait lieu. Cela explique pourquoi vous obtenez 1893-03-31. L'objet Date imprime April 1st probablement parce que l'ancienne API (java.util.TimeZone) ne contient pas tous les décalages de l'historique, donc il suppose que c'est +01:00.

Une alternative pour faire ce travail est d'utiliser toujours UTC comme le fuseau horaire:

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 

Cela obtenir la date locale correcte: 1893-04-01.


Mais pour les dates avant 1582-10-15, le code ne fonctionne pas au-dessus. C'est la date à laquelle le calendrier grégorien a été introduit. Avant cela, le calendrier julien a été utilisé, et date avant lui need an adjustment.

Je pourrais le faire avec le ThreeTen Extra project (une extension de java.time classes, créé par le même type BTW). Dans l'ensemble org.threeten.extra.chrono il y a les classes JulianChronology et 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 

La sortie sera:

Julian AD 1400-04-01

Maintenant, nous devons convertir le JulianDate à LocalDate. Si je fais LocalDate.from(julianDate) il convertit au calendrier grégorien (et le résultat est 1400-04-10).

Mais si vous voulez créer un LocalDate exactement 1400-04-01, vous devez faire ceci:

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 

La sortie sera:

1400-04-01

Sachez que les dates avant 1582-10-15 ont cet ajustement et SimpleDateFormat ne peut pas gérer ces cas p correctement. Si vous devez travailler uniquement avec 1400-04-01 (valeurs année/mois/jour), utilisez un LocalDate. Mais si vous avez besoin de le convertir en java.util.Date, sachez que ce n'est peut-être pas la même date (en raison des ajustements grégorien/julien).


Si vous ne souhaitez pas ajouter une autre dépendance, vous pouvez également effectuer toutes les opérations mathématiques à la main. J'ai adapté le code de ThreeTen, mais l'OMI l'idéal est d'utiliser l'API lui-même (comme il peut couvrir les cas de coin et d'autres choses que je manque probablement en copiant simplement un morceau de code):

// 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 

la sortie sera:

1400-04-01

Juste rappeler que tous ces calculs n'est pas nécessaire pour les dates après 1582-10-15.


Quoi qu'il en soit, si vous avez une entrée String et que vous voulez analyser, ne pas utiliser SimpleDateFormat - vous pouvez utiliser LocalDate.parse() à la place. Ou LocalDate.of(year, month, day) si vous connaissez déjà les valeurs.

Mais la conversion de ces dates locales de/vers java.util.Date est plus compliquée, car Date représente l'horodatage complet millis et les dates peuvent varier en fonction du système de calendrier utilisé.

+1

Je ne cherche pas à analyser une entrée String, mais à convertir une Date existante en LocalDate. J'ai seulement utilisé la chaîne SimpleDateFormat pour créer une date pour le test. –

+0

@HengruiJiang J'explique aussi comment le convertir, en utilisant 'JulianChronology'. –

+1

Existe-t-il une solution qui n'inclut pas de dépendances supplémentaires? Je préférerais vraiment ne pas avoir à ajouter de dépendances ... –

3

semble être un bug connu qui ne sera pas fixe: https://bugs.openjdk.java.net/browse/JDK-8061577

Après beaucoup de recherches, j'ai donné avec chaque méthode simple API et juste convertir la main. Vous pouvez envelopper la date dans un sql.Date et appeler toLocalDate() ou vous utilisez simplement les mêmes méthodes obsolètes que sql.Date. Sans méthodes dépréciées dont vous avez besoin pour convertir votre util.Date au calendrier et obtenir les champs un par un:

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)); 

Si vous Futher voulez avoir une conversion de l'année à deux chiffres comme dans SimpleDateFormat (convertir la date dans la gamme de maintenant - 80 ans jusqu'à maintenant + 19 ans), vous pouvez utiliser cette implémentation:

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)); 

Conclusion: il est laid et je vraiment ne peux pas croire qu'il n'y a pas une API simple!

+0

Cette solution aurait suffi dans mon cas, mais j'ai accepté la réponse de Hugos car il a fourni des informations plus détaillées –