JodaTime 或 Java 8 是否有特殊支持 JD Edwards 日期和时间?

Does JodaTime or Java 8 have special support JD Edwards Date and Time?

手头的主题是在 Oracle 的 ERP 软件 JD Edwards 中处理日期的混乱的特定于域的问题。其详细信息记录在 this question.

在编写用于处理来自 JD Edwards 的日期和时间的包装器 类 之前,我想知道 JodaTime 或 Java 8 是否引入了对这种独特时间格式的任何特殊支持,或者我是否'不管我使用什么库,我都必须进行大量的字符串操作。

这是一个晦涩难懂的问题,因此请仅在您对此问题有具体了解的情况下做出回应,and/or JodaTime/Java 8/JSR 310。

补充: 根据 Basil Bourque 的要求,添加伴随上述日期的时间戳示例。以下是来自不同表的 date/time 字段的两个示例:

JCSBMDATE:115100, JCSBMTIME:120102.0

RLUPMJ:114317, RLUPMT:141805.0

此外,日期变量被转换为 BigDecimal,时间是 Double。所以,我可能会保留字符串解析器,但也会编写原生采用 BigDecimal/Double 值的工厂方法。

看来时间字段实际上是从一天开始算起的毫秒数(不是秒数),“.0”可以忽略。因此,必须像这样执行转换和计算:

localDate.atTime(LocalTime.ofNanoOfDay(Long.parseLong(jdeTime) * 1000000))

否:Joda Time 和 Java8 都不支持 JD Edwards 时间表示。

JD Edwards 日期定义

其实详细一个JD Edwards date is not so gory, according to this simple description on a page at Oracle.com:

About the Julian Date Format

Date fields in JD Edwards World files are stored in the Julian format. …

The Julian (*JUL) date format is CYYDDD, where:

C is added to 19 to create the century, i.e. 0 + 19 = 19, 1 + 19 = 20. YY is the year within the century, DDD is the day in the year.

条款:

  • 我将 C 部分称为“世纪偏移量”,即要添加到 19 的世纪数。 19xx 年使用 020xx 年使用 1
  • java.time 框架将 DDD 称为“DayOfYear”,“序号日期”是另一个术语。使用“Julian”表示一年内的天数很常见但不正确,与 Julian Day.
  • 相冲突

java.time 框架不包括对解析或生成这种格式的字符串的直接支持,我找不到。

JulianFields

java.time.temporal.JulianFields,但这些是 Julian dates 的重新定义版本,我们从纪元(1970-01-01 (ISO) 而不是历史性的 11 月开始计算天数24, 4714 BC (proleptic Gregorian)),同时完全忽略年份。所以这与 JD Edwards 的定义无关,与问题中链接的该页面上的一些不正确建议相反。

序号日期

这个 JD Edwards 日期是 ordinal date 的一个版本。序数日期有时被随意(并且错误地)称为“julian”日期,只是因为它共享计算一系列天数的想法。但是一个序数日期计算从年初到年底的天数,这个数字总是在 1 到 365/366(闰年)之间,从某个纪元开始计算,并增长到数千。

回到问题,处理 java.time 中的 JD Edwards 日期…

不,我没有发现 java.time 内置的 JD Edwards 日期有任何直接或间接的支持。

java.date.format 包似乎不知道日期的世纪,只知道年份和纪元。所以我找不到定义 JD Edwards 日期的 C 部分的方法。

JD Edwards 日期的最后一部分,即一年中的序号天数,在日期时间 classes 和格式 classes 中都得到了很好的处理。

换行LocalDate

由于 JD Edwards 日期显然与 java.time 使用的 ISO 年表具有相同的逻辑,手头唯一真正的问题是根据这种特定格式解析和生成 String 对象。 LocalDate.

可以利用所有其他行为

由于我找不到为此目的定义 java.time.format.DateTimeFormatter 的方法,我建议编写一个实用程序 class 来处理这些琐事。

理想情况下,我们会扩展 LocalDate class, overriding its parse and toString 方法。也许还有一个 getCenturyOffset 方法。但是 LocalDate class 被标记为 final 并且无法扩展。所以我会创建如下所示的 class ,包装 LocalDate.

警告: 使用风险自负。新鲜代码,勉强 运行,几乎没有测试。仅作为示例,不适用于生产。按照ISC License.

的条款使用
package com.example.whatever;

import java.time.LocalDate;
import java.time.ZoneId;

/**
 * Wraps a 'LocalDate' to provide parsing/generating of strings in format known
 * as JD Edwards date.
 *
 * Format is CYYDDD where C is the number of centuries from 1900, YY is the year
 * within that century, and DDD is the ordinal day within the year (1-365 or
 * 1-366 in Leap Year).
 *
 * Immutable object. Thread-safe (hopefully! No guarantees).
 *
 * I would rather have done this by extending the 'java.time.LocalDate' class, but that class is marked 'final'.
 *
 * Examples: '000001' is January 1 of 1900. '116032' is February 1, 2016.
 *
 * © 2016 Basil Bourque. This source code may be used according to terms of the ISC License at https://opensource.org/licenses/ISC
 *
 * @author Basil Bourque
 */
public class JDEdwardsLocalDate {

    private LocalDate localDate = null;
    private int centuryOffset;
    private int yearOfCentury;
    private String formatted = null;

    // Static Factory method, in lieu of public constructor.
    static public JDEdwardsLocalDate from ( LocalDate localDateArg ) {
        return new JDEdwardsLocalDate ( localDateArg );
    }

    // Static Factory method, in lieu of public constructor.
    static public JDEdwardsLocalDate parse ( CharSequence charSequenceArg ) {
        if ( null == charSequenceArg ) {
            throw new IllegalArgumentException ( "Passed CharSequence that is null. Message # 0072f897-b05f-4a0e-88d9-57cfd63a712c." );
        }
        if ( charSequenceArg.length () != 6 ) {
            throw new IllegalArgumentException ( "Passed CharSequence that is not six characters in length. Message # eee1e134-8ec9-4c92-aff3-9296eac1a84a." );
        }
        String string = charSequenceArg.toString ();
        // Should have all digits. Test by converting to an int.
        try {
            int testAsInteger = Integer.parseInt ( string );
        } catch ( NumberFormatException e ) {
            throw new IllegalArgumentException ( "Passed CharSequence contains non-digits. Fails to convert to an integer value. Message # 0461f0ee-b6d6-451c-8304-6ceface05332." );
        }

        // Validity test passed.
        // Parse.
        int centuryOffset = Integer.parseInt ( string.substring ( 0 , 1 ) ); // Plus/Minus from '19' (as in '1900').
        int yearOfCentury = Integer.parseInt ( string.substring ( 1 , 3 ) );
        int ordinalDayOfYear = Integer.parseInt ( string.substring ( 3 ) );
        int centuryStart = ( ( centuryOffset + 19 ) * 100 ); // 0 -> 1900. 1 -> 2000. 2 -> 2100.
        int year = ( centuryStart + yearOfCentury );
        LocalDate localDate = LocalDate.ofYearDay ( year , ordinalDayOfYear );

        return new JDEdwardsLocalDate ( localDate );
    }

    // Constructor.
    private JDEdwardsLocalDate ( LocalDate localDateArg ) {
        this.localDate = localDateArg;
        // Calculate century offset, how many centuries plus/minus from 1900.
        int year = this.localDate.getYear ();
        int century = ( year / 100 );
        this.yearOfCentury = ( year - ( century * 100 ) ); // example: if 2016, return 16.
        this.centuryOffset = ( century - 19 );
        // Format as string.
        String paddedYearOfCentury = String.format ( "%02d" , this.yearOfCentury );
        String paddedDayOfYear = String.format ( "%03d" , this.localDate.getDayOfYear () );
        this.formatted = ( this.centuryOffset + paddedYearOfCentury + paddedDayOfYear );
    }

    @Override
    public String toString () {
        return this.formatted;
    }

    public LocalDate toLocalDate () {
        // Returns a java.time.LocalDate which shares the same ISO chronology as a JD Edwards Date.
        return this.localDate;
    }

    public int getDayOfYear () {
        // Returns ordinal day number within the year, 1-365 inclusive or 1-366 for Leap Year. 
        return this.localDate.getDayOfYear();
    }

    public int getYear () {
        // Returns a year number such as 2016. 
        return this.localDate.getYear();
    }

    public int getYearOfCentury () { 
        // Returns a number within 0 and 99 inclusive.
        return this.yearOfCentury;
    }

    public int getCenturyOffset () {
        // Returns 0 for 19xx dates, 1 for 20xx dates, 2 for 21xx dates, and so on.
        return this.centuryOffset;
    }

    public static void main ( String[] args ) {
        // '000001' is January 1, 1900.
        JDEdwardsLocalDate jde1 = JDEdwardsLocalDate.parse ( "000001" );
        System.out.println ( "'000001' = JDEdwardsLocalDate: " + jde1 + " = LocalDate: " + jde1.toLocalDate () + " Should be: January 1, 1900. " );

        // '116032' is February 1, 2016.
        JDEdwardsLocalDate jde2 = JDEdwardsLocalDate.parse ( "116032" );
        System.out.println ( "'116032' = JDEdwardsLocalDate: " + jde2 + " = LocalDate: " + jde2.toLocalDate () + " Should be: February 1, 2016." );

        // Today
        LocalDate today = LocalDate.now ( ZoneId.systemDefault () );
        JDEdwardsLocalDate jdeToday = JDEdwardsLocalDate.from ( today );
        System.out.println ( "LocalDate.now(): " + today + " = JDEdwardsLocalDate: " + jdeToday + " to LocalDate: " + jdeToday.toLocalDate () );
    }

}

当运行.

'000001' = JDEdwardsLocalDate: 000001 = LocalDate: 1900-01-01 Should be: January 1, 1900.

'116032' = JDEdwardsLocalDate: 116032 = LocalDate: 2016-02-01 Should be: February 1, 2016.

LocalDate.now(): 2016-05-09 = JDEdwardsLocalDate: 116130 to LocalDate: 2016-05-09

JD Edwards 时间

至于 JD Edwards 时间格式,我搜索过但找不到任何文档。如果您知道一些,请编辑您的问题以添加链接。唯一提到的 JDE 时间似乎是从午夜算起的秒数。

如果是这种情况(自午夜以来的计数),java.time.LocalTime class 已涵盖。 LocalTime 可以实例化并读取为:

纳秒分辨率意味着最多 9 位小数。处理您提到的六位数字没问题。只需计算 multiply/divide 乘以 1_000L。请注意,这意味着可能会丢失数据,因为如果 LocalTime 值来自 JD Edwards 外部,您可能会 t运行 计算小数的最后三位数字(小数的第 7、8、9 位)数据。 [仅供参考,旧的 java.util.Date/.Calendar classes 以及 Joda-Time 仅限于毫秒分辨率,对于三位小数。]

不推荐:你可以做一些组合class,由一个LocalDate和一个LocalTime组成。或者使用 LocalDateTime。关键问题是时区。如果 JD Edwards 日期时间始终处于某个时区(例如 UTC),那么组合和使用 OffsetDateTime 可能是有意义的。但是如果它没有特定的时区上下文,如果值只是日期时间的模糊概念而不是时间轴上的特定点,那么使用 LocalDateTime 因为它没有时区。如果 JDE 始终采用 UTC,请将 OffsetDateTime 设置为 ZoneOffset.UTC。如果要指定时区(偏移加上处理异常的规则如DST),使用ZonedDateTime.

建议:单独使用 LocalTime。我认为您不想在业务逻辑中使用我的 JDEdwardsLocalDate class,尤其是因为它不是适合 java.time 框架的完整实现。 我的意图是在遇到 JDE 日期时使用 class 立即将 转换为 LocalDate。 JDE 时间也是如此,使用 UTC 转换为 LocalTime immediately. If their context is always UTC, create an OffsetDateTime,然后将其传递给您的业务逻辑。仅在必要时返回到 JDE 日期和时间(坚持该 JDE 类型的数据库列,或向期望该 JDE 表示的用户报告)。

OffsetDateTime odt = OffsetDateTime.of( myLocalDate , myLocalTime , ZoneOffset.UTC );

如果 JDE 日期和时间隐含了一些其他上下文,则分配预期的时区。

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.of( myLocalDate , myLocalTime , zoneId );

时区在这里很重要。您必须大体理解这些概念。请注意,LocalDateLocalTime 以及 LocalDateTime 而不是 时间轴上的一个时刻。 它们没有特定含义,直到您将它们调整到一个时区(或至少一个 offset-from-UTC)。

如果您不熟悉 java.time 类型,我在 中包含的日期时间类型图表可能会对您有所帮助。

并且您必须了解 JDE 日期和时间的含义及其在 apps/databases 中的使用。由于找不到有关 JDE 时间的任何信息,因此我无法了解 JD Edwards 对时区的意图。所以我无法提出更具体的建议。