如何使用Java 8 DateTime API转换修改过的Julian Day数字

时间:2015-08-05 01:54:13

标签: java date datetime julian

我有一个数据库,它将Dates和DateTimes(分别作为INTEGER和DOUBLEs)存储为Modified Julian Day Numbers(MJD)。修改后的朱利安日数是从1858年11月17日午夜开始的连续天数。根据定义,它们总是以UTC计算,与GMT的偏移量为+0:00,并且不适用于夏令时。这些属性简化了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> )
            );   
}

问题显示在箭头上。使用ofEpochSecond方法构造一个LocalDateTime实例似乎需要事先知道偏移量,这似乎违反直觉(我已经有本地时间和时区,这是我想要的偏移量)。

我没有成功找到一种简单的方法来使用Java 8 API从当地时间回到UTC获得偏移量。虽然我可以继续使用旧的Calendar API,但新的DateTime库提供了引人注目的优势......所以我想尝试解决这个问题。我错过了什么?

编辑:这是一个使用旧的Java Calendar API的示例,说明如何将任意时区中的天数和小数天数“解除区域化”为UTC。此方法采用double,即“区域化日期数”和时区对象。它使用GregorianCalendar将参数转换为Epoch的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);          }

1 个答案:

答案 0 :(得分:1)

根据您的评论,您的核心问题似乎是将没有时区的日期时间(LocalDateTime)转换为分区时刻(ZonedDateTime)的模糊性。您解释夏令时(DST)等异常可能导致无效值。

ZonedDateTime zdt = myLocalDateTime.atZone( myZoneId );

这是事实。登陆DST“Spring-forward”或“Fall-back”切换时没有完美的解决方案。但是,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比“One True Time”的角色扮演。所以我建议坚持UTC。

当您需要考虑某个地区的挂钟时间时,例如您所在地区年终的Medicare示例,请确定区域挂钟时间,然后转换为UTC。根据定义,java.time中的Instant类始终为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。使用doubleDoublefloatFloat意味着使用浮点技术来消除准确性,从而提高性能。

以下是对从BigDecimal(Modified Julian Days)到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

     

output.toString():57831.5

查看所有code running live at IdeOne.com

此外,my Answer对类似问题可能会有所帮助。