Comment convertir les numéros de Jour Julien modifiés avec l'API Java 8 DateTime


J'ai une base de données qui stocke les Dates et les DateTimes (sous forme d'entiers et de DOUBLEs, respectivement) sous forme de Nombres de Jours juliens modifiés (MJD). Les nombres de jours juliens modifiés sont un nombre consécutif de jours à partir de minuit UTC, 17 novembre 1858. Par définition, ils sont toujours comptés en UTC, ont un décalage de +0:00 par rapport à GMT et ne s'ajustent pas aux heures d'été. Ces propriétés simplifient certaines opérations avec DateTimes telles que la priorité et l'arithmétique de date.

L'inconvénient est que MJDs doit être déplacé de UTC et délocalisé à UTC avant et après utilisation, en particulier pour les applications pour lesquelles les limites de jour sont d'une importance critique (Medicare, par exemple, reconnaît une limite de date facturable comme minuit en heure locale).

Considérons la méthode d'usine statique suivante dont le but est de délocaliser dans un MJD (en UTC) un "numéro de jour régional" (fondamentalement, un MJD qui a eu le décalage approprié ajouté pour qu'il représente un local DateTime):

public static MJD ofDayNumberInZone(double regDN, ZoneId zone) {
    :
    :            
}

Il semble intuitivement évident que si vous avez une date et une heure locales, et que vous connaissez le fuseau horaire local, vous devriez avoir toutes les informations dont vous avez besoin pour décaler regDN vers UTC (comme requis par un MJD).

En fait, cette fonction est assez simple à écrire en utilisant l'API Java Calendar précédente. Le regDN est facilement converti en un Date qui est utilisé pour définir une instance GregorianCalendar. Connaissant le" fuseau horaire local", le calendrier signale ZONE_OFFSET et DST_OFFSET valeurs qui peuvent ensuite être utilisées pour ajuster le numéro de jour dans un MJD.

C'est ma tentative d'écrire un algorithme similaire dans l'API Java 8 DateTime:

public static MJD ofDayNumberInZone(double zonedMJD, ZoneId zone) {
        double epochSec = ((zonedMJD - MJD.POSIX_EPOCH_AS_MJD) * 86400.0);
        LocalDateTime dt = LocalDateTime
            .ofEpochSecond(
                    (long) epochSec, 
                    (int) (epochSec - Math.floor(epochSec) * 1000000000.0),
--->                zone.getRules().getOffset( <Instant> )
            );   
}

Le problème est indiqué à la flèche. La construction d'une instance LocalDateTime à l'aide de la méthode ofEpochSecond semble exiger que vous connaissiez les décalages à l'avance, ce qui semble contre-intuitif (j'ai déjà l'heure locale et le fuseau horaire, c'est le décalage que je veux).

Je n'ai pas réussi à trouver un moyen simple d'obtenir les décalages de l'heure locale vers UTC à l'aide de l'API Java 8. Bien que je puisse continuer à utiliser l'ancienne API de calendrier, les nouvelles bibliothèques DateTime offrent des avantages convaincants ... donc, je voudrais essayer de comprendre cela. Ce qui me manque?


EDIT: Voici un exemple, en utilisant l'ancienne API Java Calendar, de la façon dont un nombre de jours et de jours fractionnaires dans un fuseau horaire arbitraire est "dérégionalisé" en UTC. Cette méthode prend un double qui est le "régionalisé numéro de jour " et un objet de fuseau horaire. Il utilise un GregorianCalendar pour convertir les paramètres en un nombre UTC de millisecondes à partir de l'époque:

    private static final Object             lockCal = new Object();
    private static final SimpleDateFormat       SDF = new SimpleDateFormat();
    private static final GregorianCalendar      CAL = new
            GregorianCalendar(TimeZone.getTimeZone(HECTOR_ZONE));
        :
        :

    public static MJD ofDayNumberInZone(double rdn, TimeZone tz) {
        Date dat = new Date((long) ((rdn - MJD.POSIX_EPOCH_AS_MJD) * 
                (86400.0 * 1000.0)));
        return MJD.ofDateInZone(dat, tz);
    }

    public static MJD ofDateInZone(Date dat, TimeZone tz) {
        long utcMillisFromEpoch;

        synchronized(lockCal) {
            CAL.setTimeZone(tz);
            CAL.setTime(dat);
            utcMillisFromEpoch = CAL.getTimeInMillis();
        }
        return MJD.ofEpochMillisInUTC(utcMillisFromEpoch);
    }

    public static MJD ofEpochMillisInUTC(long millis) 
        { return new MJD((millis / (86400.0 * 1000.0)) + POSIX_EPOCH_AS_MJD);          }
Author: scottb, 2015-08-05

1 answers

Selon vos commentaires, votre problème principal semble être l'ambiguïté de la conversion d'une date-heure sans fuseau horaire (a LocalDateTime) dans un moment zoné (a ZonedDateTime). Vous expliquez que des anomalies telles que l'heure d'été (DST) peuvent entraîner des valeurs non valides.

ZonedDateTime zdt = myLocalDateTime.atZone( myZoneId );

C'est vrai. Il n'y a pas de solution parfaite lors de l'atterrissage dans les cutovers DST "Spring-forward" ou "Fall-back". Cependant, la java.les classes de temps font résolvent l'ambiguïté en adoptant un certain politique. Vous pouvez ou non être d'accord avec cette politique. Mais si vous êtes d'accord, vous pouvez compter sur java.il est temps de déterminer un résultat.

Pour citer la documentation pour ZonedDateTime.ofLocal:

Dans le cas d'un chevauchement, où les horloges sont décalées, il y a deux décalages valides. Si le décalage préféré est l'un des décalages valides, il est utilisé. Sinon, le décalage valide antérieur est utilisé, correspondant généralement à"été".

Dans le cas d'un écart, où les horloges saut vers l'avant, il n'y a pas de décalage. Au lieu de cela, la date-heure locale est ajustée pour être plus tardive par la longueur de l'écart. Pour un changement typique d'heure d'été d'une heure, la date-heure locale sera déplacée une heure plus tard dans le décalage correspondant généralement à "été".

    LocalDate modifiedJulianEpoch = LocalDate.of( 1858 , 11 , 17 );
    LocalDate today = LocalDate.now( ZoneOffset.UTC );
    long days = ChronoUnit.DAYS.between (  modifiedJulianEpoch , today );

Aujourd'hui: 2017-03-19 jours: 57831

Je ne comprends pas bien vos problèmes. Mais il me semble que le but de MJD (Jours juliens modifiés) est d'avoir un moyen de suivre un " Un Vrai Time " pour éviter toute confusion des fuseaux horaires. Dans le système de calendrier standard ISO 8601, UTC joue le rôle de "One True Time". Je suggère donc de rester à UTC.

Lorsque vous devez considérer l'heure de l'horloge murale d'une région, telle que votre exemple de fin de journée de Medicare de la région, déterminez l'heure de l'horloge murale régionale, puis convertissez-la en UTC. La classe Instant en java.l'heure est toujours en UTC par définition.

ZoneId z = ZoneId.of( "America/Los_Angeles" );
LocalDate localDate = LocalDate.now( z );
ZonedDateTime firstMomentNextDay = localDate.plusDays( 1 ).atStartOfDay( z );
Instant medicareExpiration = firstMomentNextDay.toInstant(); // UTC
BigDecimal modJulDays = this.convertInstantToModifiedJulianDays( medicareExpiration ) ;

Utilisez BigDecimal lorsque vous travaillez avec des décimales fractionnaires où la précision question. Utilisation double, Double, float, ou Float signifie utiliser une technologie à virgule flottante qui échange la précision pour des performances plus rapides.

Voici une coupe approximative à un code pour faire la conversion de BigDecimal (Jours Juliens modifiés) en Instantané. Je suppose qu'une personne intelligente pourrait trouver une version plus maigre ou plus méchante de ce code, mais mon code ici semble fonctionner. Utilisez à vos propres risques. J'ai à peine testé ce code.

public Instant convertModifiedJulianDaysToInstant ( BigDecimal modJulDays ) {
    Instant epoch = OffsetDateTime.of ( 1858, 11, 17, 0, 0, 0, 0, ZoneOffset.UTC ).toInstant ( ); // TODO: Make into a constant to optimize.
    long days = modJulDays.toBigInteger ( ).longValue ( );
    BigDecimal fractionOfADay = modJulDays.subtract ( new BigDecimal ( days ) ); // Extract the fractional number, separate from the integer number.
    BigDecimal secondsFractional = new BigDecimal ( TimeUnit.DAYS.toSeconds ( 1 ) ).multiply ( fractionOfADay );
    long secondsWhole = secondsFractional.longValue ( );
    long nanos = secondsFractional.subtract ( new BigDecimal ( secondsWhole ) ).multiply ( new BigDecimal ( 1_000_000_000L ) ).longValue ( );
    Duration duration = Duration.ofDays ( days ).plusSeconds ( secondsWhole ).plusNanos ( nanos );
    Instant instant = epoch.plus ( duration );
    return instant;
}

Et aller de l'autre direction.

public BigDecimal convertInstantToModifiedJulianDays ( Instant instant ) {
    Instant epoch = OffsetDateTime.of ( 1858, 11, 17, 0, 0, 0, 0, ZoneOffset.UTC ).toInstant ( ); // TODO: Make into a constant to optimize.
    Duration duration = Duration.between ( epoch, instant );
    long wholeDays = duration.toDays ( );
    Duration durationRemainder = duration.minusDays ( wholeDays );

    BigDecimal wholeDaysBd = new BigDecimal ( wholeDays );
    BigDecimal partialDayInNanosBd = new BigDecimal ( durationRemainder.toNanos ( ) ); // Convert entire duration to a total number of nanoseconds.
    BigDecimal nanosInADayBd = new BigDecimal ( TimeUnit.DAYS.toNanos ( 1 ) );  // How long is a standard day in nanoseconds?
    int scale = 9; // Maximum number of digits to the right of the decimal point.
    BigDecimal partialDayBd = partialDayInNanosBd.divide ( nanosInADayBd ); // Get a fraction by dividing a total number of nanos in a day by our partial day of nanos.
    BigDecimal result = wholeDaysBd.add ( partialDayBd );
    return result;
}

Appeler ces méthodes de conversion.

    BigDecimal input = new BigDecimal ( "57831.5" );
    Instant instant = this.convertModifiedJulianDaysToInstant ( input );
    BigDecimal output = this.convertInstantToModifiedJulianDays ( instant );

Vidage sur console.

    System.out.println ( "input.toString(): " + input );
    System.out.println ( "instant.toString(): " + instant );
    System.out.println ( "output.toString(): " + output );

Entrée.toString (): 57831.5

Éphémère.toString (): 2017-03-19T12:00: 00Z

Sortie.toString (): 57831.5

Voir tout ce code en cours d'exécution en direct à IdeOne.com.

Aussi, ma réponse à une question similaire peut être utile.

 1
Author: Basil Bourque, 2017-05-23 10:30:00