【问题标题】:How to Convert Modified Julian Day Numbers with the Java 8 DateTime API如何使用 Java 8 DateTime API 转换修改后的儒略日数
【发布时间】:2017-03-20 13:12:54
【问题描述】:

我有一个将日期和日期时间(分别作为整数和双精度数)存储为修改儒略日数 (MJD) 的数据库。修改后的儒略日数是从 1858 年 11 月 17 日午夜 UTC 开始的连续天数。根据定义,它们始终以 UTC 计算,与 GMT 有 +0:00 的偏移量,并且不针对夏令时进行调整。这些属性简化了 DateTimes 的某些操作,例如优先级和日期算术。

缺点是 MJD 必须在使用前后从 UTC 重新定位并重新定位回 UTC,特别是对于日期边界至关重要的应用程序(例如,Medicare 将计费日期边界识别为 -local- 中的午夜)时间)。

考虑以下静态工厂方法,其目的是将“区域天数”(基本上,已添加适当偏移量以表示本地日期时间的 MJD)转移到 MJD(UTC)中:

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

如果您有一个本地日期和时间,并且您知道本地时区,那么您应该拥有将 regDN 偏移回 UTC 所需的所有信息(根据 MJD 的要求),这似乎很直观)。

事实上,使用以前的 Java 日历 API 编写此函数相当简单。 regDN 很容易转换为用于设置 GregorianCalendar 实例的 Date。知道“本地时区”后,日历会报告 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 日历 API 的示例,说明如何将任意时区中的天数和小数天数“非区域化”为 UTC。此方法采用双精度,即“区域化日期数”和时区对象。它使用 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);          }

【问题讨论】:

  • 那么从Zoned MJD 转换为UTC MJD 的方法的目的是什么?
  • 我不使用 Java,但让我们想象你成功了。我希望您能够在程序运行时获得有效的偏移量,或者可能考虑日期并更改当年夏令时的偏移量。当 DST 的开始/结束日期不同时,我不希望 Java 能够提供历史偏移量,当然,预测国会未来的行动是不可能的。那么您是否对历史或未来的日期/时间进行了适当的考虑?
  • @GerardAshton:这与我的问题无关,但 Java DateTime API 完全支持 IANA 时区数据库,并为特定于区域的时间偏移计算提供历史上准确的偏移。
  • 我注意到 getOffset 方法需要一个瞬间,它是从纪元以来的秒数和纳秒数创建的。但是,如果您不知道偏移量并且不知道格林威治/UTC 时间,那么您就无法计算自纪元以来的秒数,因此您无法创建瞬间。
  • @GerardAshton:这是我遇到的悖论......如果 - 你 - 知道当地时间和当地时区,那么 - 你 - 有足够的信息来正确到达 UTC。然而,Java 8 DateTime API 似乎无法构造任何 Instant,除非它知道如何在内部以 UTC 表示它(显然,事先知道与 UTC 的偏移量作为先决条件)。我被困在如何获取当地时间和时区并从中制作即时信息。这在旧 API 中很简单,但我发现新 API 很难使用。

标签: java date datetime julian-date


【解决方案1】:

根据您的 cmets,您的核心问题似乎是关于将没有时区的日期时间 (a LocalDateTime) 转换为分区时刻 (a ZonedDateTime) 的歧义。您解释说,夏令时 (DST) 等异常可能导致无效值。

ZonedDateTime zdt = myLocalDateTime.atZone( myZoneId );

这是真的。登陆 DST “Spring-forward” 或 “Fall-back” 切换时没有完美的解决方案。然而,java.time 类 do 通过采用某种策略来解决歧义。您可能同意也可能不同意该政策。但如果您同意,那么您可以依靠 java.time 来确定结果。

引用ZonedDateTime.ofLocal的文档:

在重叠的情况下,时钟被设置回来,有两个有效的偏移量。如果首选偏移量是有效偏移量之一,则使用它。否则使用较早的有效偏移量,通常对应于“summer”。

在时钟向前跳的间隙的情况下,没有有效的偏移量。相反,本地日期时间会根据间隙的长度进行调整。对于典型的一小时夏令时更改,本地日期时间将在一小时后移动到通常对应于“夏季”的偏移量中。

    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。

当您需要考虑某个地区的挂钟时间时,例如您所在地区的医疗保险示例,请确定该地区的挂钟时间,然后将其转换为 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(修改后的儒略日)转换为 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 对类似问题可能会有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-02-05
    • 1970-01-01
    • 1970-01-01
    • 2020-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-11
    相关资源
    最近更新 更多