Как преобразовать измененные числа дней по юлианскому календарю с помощью Java 8 DateTime API

У меня есть база данных, в которой хранятся даты и даты и время (как INTEGER и DOUBLE, соответственно) как модифицированные числа дней по юлианскому календарю (MJD). Модифицированные числа дней по юлианскому календарю представляют собой последовательный подсчет дней с полуночи по Гринвичу 17 ноября 1858 года. По определению они всегда отсчитываются в формате UTC, имеют смещение +0:00 от GMT и не учитывают переход на летнее время. Эти свойства упрощают определенные операции с DateTimes, такие как приоритет и арифметика дат.

Недостатком является то, что MJD должны быть перемещены из UTC и делокализованы обратно в UTC до и после использования, особенно для приложений, для которых критически важны границы дня (например, Medicare распознает границу оплачиваемой даты как полночь в -местном-времени).

Рассмотрим следующий статический фабричный метод, целью которого является делокализовать в MJD (в формате UTC) «региональный номер дня» (по сути, MJD, к которому добавлено соответствующее смещение, чтобы он представлял локальный DateTime):

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

Кажется интуитивно очевидным, что если у вас есть местная дата и время и вы знаете местный часовой пояс, у вас должна быть вся необходимая информация, чтобы сместить regDN обратно в UTC (как того требует MJD).

На самом деле эту функцию довольно просто написать, используя предыдущий Java Calendar API. regDN легко преобразуется в Date, который используется для установки экземпляра GregorianCalendar. Зная «местный часовой пояс», календарь сообщает значения ZONE_OFFSET и DST_OFFSET, которые затем можно использовать для корректировки номера дня в MJD.

Это моя попытка написать аналогичный алгоритм в Java 8 DateTime API:

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

Проблема указана стрелкой. Построение экземпляра LocalDateTime с использованием метода ofEpochSecond, по-видимому, требует, чтобы вы знали смещения заранее, что кажется нелогичным (у меня уже есть местное время и часовой пояс, это смещение, которое я хочу).

Мне не удалось найти простой способ получить смещения от местного времени до UTC с помощью API Java 8. Хотя я мог бы продолжать использовать старый API календаря, новые библиотеки DateTime предлагают неоспоримые преимущества... поэтому я хотел бы попытаться выяснить это. Что мне не хватает?


РЕДАКТИРОВАТЬ: Вот пример использования старого API календаря Java о том, как количество дней и дробных дней в произвольном часовом поясе «дерегионализируется» в UTC. Этот метод принимает двойное значение, которое является «региональным номером дня» и объектом часового пояса. Он использует GregorianCalendar для преобразования параметров в миллисекунды UTC от эпохи:

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

person scottb    schedule 05.08.2015    source источник
comment
Так является ли целью метода преобразование из Zoned MJD в UTC MJD?   -  person fdsa    schedule 05.08.2015
comment
Я не использую Java, но давайте представим, что вы преуспели. Я ожидаю, что вы сможете получить смещение, действующее при запуске программы, или, возможно, принять во внимание дату и изменить смещение для перехода на летнее время для текущего года. Я не ожидал, что Java сможет предоставить исторические смещения назад, когда даты начала/окончания перехода на летнее время были другими, и, конечно же, невозможно предсказать действия Конгресса в будущем. Итак, делаете ли вы надлежащие поправки на историческую или будущую дату/время?   -  person Gerard Ashton    schedule 05.08.2015
comment
@GerardAshton: Это отдельный вопрос, не связанный с моим вопросом, однако Java DateTime API полностью поддерживает базу данных часовых поясов IANA и обеспечивает исторически точные смещения для расчетов смещения времени для конкретной зоны.   -  person scottb    schedule 05.08.2015
comment
Я заметил, что для метода getOffset требуется мгновение, которое создается из секунд с начала эпохи и наносекунд. Но если вы не знаете смещение и не знаете время по Гринвичу/UTC, то вы не можете вычислить секунды с начала эпохи, поэтому вы не можете создать момент.   -  person Gerard Ashton    schedule 06.08.2015
comment
@GerardAshton: вот парадокс, с которым я сталкиваюсь ... если вы знаете местное время и местный часовой пояс, то у вас достаточно информации, чтобы правильно определить UTC. Однако Java 8 DateTime API, по-видимому, не может создать какой-либо Instant, если он не знает, как представить его внутренне в UTC (очевидно, с предварительным знанием смещения относительно UTC в качестве предварительного условия). Я застрял в том, как взять местное время и часовой пояс и сделать из него Instant. Это просто в старом API, но мне сложно использовать новый API.   -  person scottb    schedule 06.08.2015
comment
@fdsa: Да, это краткое изложение проблемы: взять зонированный MJD (если вы согласны с тем, что такая вещь существует; все MJD должны быть в формате UTC) и получить от него UTC MJD.   -  person scottb    schedule 06.08.2015
comment
Я просмотрел документацию по дате Java 8. . Кажется, есть два вида не-UTC/гринвичского времени. LocalDateTime не связан пакетом Java.time с каким-либо конкретным часовым поясом, поэтому его необходимо выяснить из контекста или исходного кода, не связанного с Java.time. ZonedDateTime — это местное время, для которого Java.time отслеживает зону; Java знает, что такое правила. Если вы исследуете ZonedDateTime, вы можете найти то, что хотите. Есть случай, когда преобразование невозможно; local--›UTC при осеннем переходе на летнее время.   -  person Gerard Ashton    schedule 06.08.2015


Ответы (1)


Согласно вашим комментариям, ваша основная проблема, по-видимому, связана с двусмысленностью преобразования даты и времени без часового пояса (LocalDateTime) в зонированный момент (ZonedDateTime). Вы объясняете, что аномалии, такие как переход на летнее время (DST), могут привести к недопустимым значениям.

ZonedDateTime zdt = myLocalDateTime.atZone( myZoneId );

Это верно. Не существует идеального решения при посадке в переходах DST «пружина вперед» или «откат назад». Однако классы java.time делают разрешают двусмысленность, принимая определенную политику. Вы можете соглашаться или не соглашаться с этой политикой. Но если вы согласны, то вы можете положиться на java.time для определения результата.

Чтобы процитировать документацию для ZonedDateTime.ofLocal:

В случае перекрытия, когда часы переведены назад, есть два действительных смещения. Если предпочтительное смещение является одним из допустимых смещений, оно используется. В противном случае используется более раннее допустимое смещение, обычно соответствующее «лету».

В случае промежутка, когда часы перескакивают вперед, допустимого смещения нет. Вместо этого локальная дата-время корректируется на более позднюю на длину промежутка. Для типичного перехода на летнее время на один час местная дата-время будет перемещена на час позже на смещение, обычно соответствующее «лету».

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

сегодня: 2017-03-19 дней: 57831

Я не совсем понимаю ваши проблемы. Но мне кажется, что смысл MJD (модифицированных юлианских дней) в том, чтобы иметь способ отслеживать «Единое истинное время», чтобы избежать путаницы с часовыми поясами. В стандартной календарной системе ISO 8601 UTC играет роль «единого истинного времени». Поэтому я предлагаю придерживаться UTC.

Если вам нужно учитывать время по настенным часам в регионе, например, в вашем примере Medicare для конца дня в регионе, определите региональное время по настенным часам, а затем преобразуйте его в UTC. Класс Instant в java.time всегда находится в формате UTC по определению.

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

Используйте BigDecimal при работе с десятичными дробями, где важна точность. Использование double, Double, float или Float означает использование технологии с плавающей запятой, которая жертвует точностью ради более высокой производительности.

Вот черновик кода для преобразования из BigDecimal (модифицированные юлианские дни) в Instant. Я предполагаю, что какой-нибудь умный человек мог бы найти более компактную или более грубую версию этого кода, но мой код здесь, похоже, работает. Используйте на свой риск. Я вообще почти не тестировал этот код.

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

И идти в другом направлении.

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

Вызов этих методов преобразования.

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

Дамп на консоль.

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

input.toString(): 57831,5

Instant.toString(): 2017-03-19T12:00:00Z

вывод.toString(): 57831,5

Посмотрите весь этот код, работающий в режиме реального времени, на IdeOne.com.

Кроме того, может быть полезен мой ответ на аналогичный вопрос.

person Basil Bourque    schedule 20.03.2017