使用新的Java8DateTimeFormatter进行严格的日期解析

使用新的Java8DateTimeFormatter进行严格的日期解析,java,parsing,date,datetime,java-8,Java,Parsing,Date,Datetime,Java 8,我有一个简单的问题:我想严格地解析格式为“yyyyMMdd”的Java字符串,以便“19800229”是有效日期,但“19820229”不是。假设这些是正常公历的广告日期 我试图使用JDK 8中新的java.time包来解决这个问题,但事实证明它比预期的更复杂。我目前的代码是: private static final DateTimeFormatter FORMAT = DateTimeFormatter .ofPattern("yyyyMMdd").withChronolog

我有一个简单的问题:我想严格地解析格式为
“yyyyMMdd”
的Java字符串,以便
“19800229”
是有效日期,但
“19820229”
不是。假设这些是正常公历的广告日期

我试图使用JDK 8中新的
java.time
包来解决这个问题,但事实证明它比预期的更复杂。我目前的代码是:

private static final DateTimeFormatter FORMAT = DateTimeFormatter
        .ofPattern("yyyyMMdd").withChronology(IsoChronology.INSTANCE)
        .withResolverStyle(STRICT);

public static LocalDate parse(String yyyyMMdd) {
    return LocalDate.parse(yyyyMMdd, FORMAT);
}
但是,解析有效日期(如“19800228”)会产生我认为不可理解的错误:

java.time.format.DateTimeParseException:无法分析文本“19820228”:无法从TemporalAccessor获取LocalDate:{MonthOfYear=2,DayOfMonth=28,YearOfEra=1982},ISO类型为java.time.format.parsed


如何使用
java.time.format.DateTimeFormatter
来解决我的简单用例?

我正在编辑,以通过使用使用DateTimeFormatterBuilder创建的自定义格式化程序来限制哪种字符串被认为是有效的

public class DateFormmaterTest {

    static DateTimeFormatter CUSTOM_BASIC_ISO_DATE = new DateTimeFormatterBuilder()
            .parseCaseInsensitive().appendValue(YEAR, 4)
            .appendValue(MONTH_OF_YEAR, 2).appendValue(DAY_OF_MONTH, 2)
            .optionalStart().toFormatter()
            .withResolverStyle(ResolverStyle.STRICT)
            .withChronology(IsoChronology.INSTANCE);

    public static void main(String[] args) {

        LocalDate date1 = LocalDate.parse("19800228-5000",
                CUSTOM_BASIC_ISO_DATE);

        System.out.println(date1);

    }
}
1982年2月29日无效,将引发以下事件:

Caused by: java.time.DateTimeException: Invalid date 'February 29' as '1982' is not a leap year
    at java.time.LocalDate.create(LocalDate.java:429)
Exception in thread "main" java.time.format.DateTimeParseException: Text '19800228-5000' could not be parsed, unparsed text found at index 8. 
日期19800228-5000将与基本ISO日期一起使用,因为它允许您不希望允许的可选偏移量。我的自定义_BASIC_ISO_日期格式化程序将不允许这样做,并抛出以下内容:

Caused by: java.time.DateTimeException: Invalid date 'February 29' as '1982' is not a leap year
    at java.time.LocalDate.create(LocalDate.java:429)
Exception in thread "main" java.time.format.DateTimeParseException: Text '19800228-5000' could not be parsed, unparsed text found at index 8. 

注意,如果您确定字符串长度yyyyMMdd,那么您可以始终使用前8个字符的子字符串来消除对解析器的需求。然而,这是两件不同的事情。解析程序将在输入时标记无效的日期格式,子字符串当然会去掉多余的字符。

尝试使用格式“uummdd”代替。

Java8使用
uuu
表示年份,而不是
yyyy
。在Java 8中,
yyyy
表示“纪年”(公元前或公元后),错误消息抱怨MonthOfYear、DayOfMonth和YearOfEra没有足够的信息来构造日期,因为纪年未知

要解决此问题,请在格式字符串中使用
uuu
,例如模式(“uummdd”)的日期时间格式

或者,如果要继续使用
yyyy
,可以设置默认纪元,例如:

private static final DateTimeFormatter FORMAT = new DateTimeFormatterBuilder()
            .appendPattern("yyyyMMdd")
            .parseDefaulting(ChronoField.ERA, 1 /* era is AD */)
            .toFormatter()
            .withChronology(IsoChronology.INSTANCE)
            .withResolverStyle(ResolverStyle.STRICT);

我最近也有类似的要求。我正在检修一个遗留应用程序,该应用程序是在Java5成为最新版本时编写的。我需要继续解析特定的日期和日期/时间格式,但我想要旧的
SimpleDateFormat
类所提供的“严格”行为。我还需要在
java.util.Date
之间进行转换,以与旧的现有代码兼容

下面是我的实用程序类和单元测试:

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.Date;

/**
 * Static utility class for converting from java.util.Date to modern Java time classes,
 * and some predefined DateTimeFormatter instances to handle formats used in data files
 * that this application processes.
 */
public class DateTimeUtils {

    private DateTimeUtils() {}

    // Jim Tough - 2020-09-14
    // These two date/time formats are part of the data file specification.
    // We are stuck with these formats now.
    private static final String DTF_YYYYMMDDHHMMSS_FORMAT_STRING = "uuuuMMdd HH:mm:ss";
    private static final String DTF_YYYYMMDDHHMM_FORMAT_STRING = "uuuuMMddHHmm";
    private static final String DTF_YYYYMMDD_FORMAT_STRING = "uuuuMMdd";

    public static final DateTimeFormatter DTF_YYYYMMDDHHMMSS =
            DateTimeFormatter.ofPattern(DTF_YYYYMMDDHHMMSS_FORMAT_STRING)
                    .withResolverStyle(ResolverStyle.STRICT);

    public static final DateTimeFormatter DTF_YYYYMMDDHHMM =
            DateTimeFormatter.ofPattern(DTF_YYYYMMDDHHMM_FORMAT_STRING)
                    .withResolverStyle(ResolverStyle.STRICT);

    public static final DateTimeFormatter DTF_YYYYMMDD =
            DateTimeFormatter.ofPattern(DTF_YYYYMMDD_FORMAT_STRING)
                    .withResolverFields(ChronoField.YEAR, ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH)
                    .withResolverStyle(ResolverStyle.STRICT);

    //-------------------------------------------------------------------------

    public static LocalDate convertToLocalDateViaInstant(Date dateToConvert) {
        if (dateToConvert == null) {
            return null;
        }
        return dateToConvert.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDate();
    }

    public static LocalDateTime convertToLocalDateTimeViaInstant(Date dateToConvert) {
        if (dateToConvert == null) {
            return null;
        }
        return dateToConvert
                .toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();
    }

    public static Date convertToDateViaInstant(LocalDate dateToConvert) {
        if (dateToConvert == null) {
            return null;
        }
        return java.util.Date.from(dateToConvert.atStartOfDay()
                .atZone(ZoneId.systemDefault())
                .toInstant());
    }

    public static Date convertToDateViaInstant(LocalDateTime dateToConvert) {
        if (dateToConvert == null) {
            return null;
        }
        return java.util.Date.from(
                dateToConvert.atZone(ZoneId.systemDefault()).toInstant()
        );
    }

    //-------------------------------------------------------------------------

    /**
     * Parse the supplied string using the supplied {@code DateTimeFormatter} and return the result
     * as a {@code LocalDate}
     * @param dtf Non-null
     * @param s String, or null value
     * @return {@code LocalDate}, or null if the supplied string parameter is null
     * @throws java.time.DateTimeException when supplied string cannot be parsed by the
     *   {@code DateTimeFormatter} and converted to a {@code LocalDate}
     */
    public static LocalDate parseAsLocalDate(DateTimeFormatter dtf, String s) {
        if (s == null) {
            return null;
        }
        TemporalAccessor ta = dtf.parse(s);
        return LocalDate.from(ta);
    }

    /**
     * Parse the supplied string using the supplied {@code DateTimeFormatter} and return the result
     * as a {@code LocalDateTime}
     * @param dtf Non-null
     * @param s String, or null value
     * @return {@code LocalDateTime}, or null if the supplied string parameter is null
     * @throws java.time.DateTimeException when supplied string cannot be parsed by the
     *   {@code DateTimeFormatter} and converted to a {@code LocalDateTime}
     */
    public static LocalDateTime parseAsLocalDateTime(DateTimeFormatter dtf, String s) {
        if (s == null) {
            return null;
        }
        TemporalAccessor ta = dtf.parse(s);
        return LocalDateTime.from(ta);
    }

}
单元测试

import org.junit.jupiter.api.Test;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author JTough
 */
public class DateTimeUtilsTest {

    @Test
    void testConvertToLocalDateViaInstant_nominal() {
        LocalDate ld = DateTimeUtils.convertToLocalDateViaInstant(new java.util.Date());

        assertNotNull(ld);
    }

    @Test
    void testConvertToLocalDateViaInstantWithNullValue() {
        assertNull(DateTimeUtils.convertToLocalDateViaInstant(null));
    }

    //-------------------------------------------------------------------------

    @Test
    void testConvertToLocalDateTimeViaInstant_nominal() {
        LocalDateTime ldt = DateTimeUtils.convertToLocalDateTimeViaInstant(new java.util.Date());

        assertNotNull(ldt);
    }

    @Test
    void testConvertToLocalDateTimeViaInstantWithNullValue() {
        assertNull(DateTimeUtils.convertToLocalDateTimeViaInstant(null));
    }

    //-------------------------------------------------------------------------

    @Test
    void testConvertToDateViaInstant_nominal_A() {
        java.util.Date d = DateTimeUtils.convertToDateViaInstant(LocalDate.now());

        assertNotNull(d);
    }

    @Test
    void testConvertToDateViaInstantWithNullValueA() {
        assertNull(DateTimeUtils.convertToDateViaInstant((LocalDate)null));
    }

    //-------------------------------------------------------------------------

    @Test
    void testConvertToDateViaInstant_nominal_B() {
        java.util.Date d = DateTimeUtils.convertToDateViaInstant(LocalDateTime.now());

        assertNotNull(d);
    }

    @Test
    void testConvertToDateViaInstantWithNullValueB() {
        assertNull(DateTimeUtils.convertToDateViaInstant((LocalDate)null));
        assertNull(DateTimeUtils.convertToDateViaInstant((LocalDateTime)null));
    }

    //-------------------------------------------------------------------------

    @Test
    void testParseAsLocalDate_nominal_A() {
        LocalDate ld = DateTimeUtils.parseAsLocalDate(DateTimeUtils.DTF_YYYYMMDD, "20201225");

        assertNotNull(ld);
        assertEquals(2020, ld.getYear());
        assertEquals(Month.DECEMBER, ld.getMonth());
        assertEquals(25, ld.getDayOfMonth());
    }

    @Test
    void testParseAsLocalDate_nominal_B() {
        LocalDate ld = DateTimeUtils.parseAsLocalDate(DateTimeUtils.DTF_YYYYMMDD, "20200229");

        assertNotNull(ld);
        assertEquals(2020, ld.getYear());
        assertEquals(Month.FEBRUARY, ld.getMonth());
        assertEquals(29, ld.getDayOfMonth());
    }

    @Test
    void testParseAsLocalDateWithInvalidDayOfMonth() {
        assertThrows(DateTimeException.class, ()->DateTimeUtils.parseAsLocalDate(DateTimeUtils.DTF_YYYYMMDD, "20200230"));
    }

    @Test
    void testParseAsLocalDateWithGarbageInputString() {
        assertThrows(DateTimeException.class, ()->DateTimeUtils.parseAsLocalDate(DateTimeUtils.DTF_YYYYMMDD, "garbage"));
    }

    //-------------------------------------------------------------------------

    @Test
    void testParseAsLocalDateTime_withoutseconds_nominal_A() {
        LocalDateTime ldt = DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMM, "202012251359");

        assertNotNull(ldt);
        assertEquals(2020, ldt.getYear());
        assertEquals(Month.DECEMBER, ldt.getMonth());
        assertEquals(25, ldt.getDayOfMonth());
        assertEquals(13, ldt.getHour());
        assertEquals(59, ldt.getMinute());
    }

    @Test
    void testParseAsLocalDateTime_withseconds_nominal_A() {
        LocalDateTime ldt = DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMMSS, "20201225 13:59:33");

        assertNotNull(ldt);
        assertEquals(2020, ldt.getYear());
        assertEquals(Month.DECEMBER, ldt.getMonth());
        assertEquals(25, ldt.getDayOfMonth());
        assertEquals(13, ldt.getHour());
        assertEquals(59, ldt.getMinute());
        assertEquals(33, ldt.getSecond());
    }

    @Test
    void testParseAsLocalDateTime_withoutseconds_nominal_B() {
        LocalDateTime ldt = DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMM, "202002291234");

        assertNotNull(ldt);
        assertEquals(2020, ldt.getYear());
        assertEquals(Month.FEBRUARY, ldt.getMonth());
        assertEquals(29, ldt.getDayOfMonth());
        assertEquals(12, ldt.getHour());
        assertEquals(34, ldt.getMinute());
    }

    @Test
    void testParseAsLocalDateTime_withseconds_nominal_B() {
        LocalDateTime ldt = DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMMSS, "20200229 12:34:56");

        assertNotNull(ldt);
        assertEquals(2020, ldt.getYear());
        assertEquals(Month.FEBRUARY, ldt.getMonth());
        assertEquals(29, ldt.getDayOfMonth());
        assertEquals(12, ldt.getHour());
        assertEquals(34, ldt.getMinute());
        assertEquals(56, ldt.getSecond());
    }

    @Test
    void testParseAsLocalDateTimeWithInvalidDayOfMonth() {
        assertThrows(DateTimeException.class, ()->DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMM, "202002301234"));
    }

    @Test
    void testParseAsLocalDateTimeWithInvalidHour() {
        assertThrows(DateTimeException.class, ()->DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMM, "202012252500"));
    }

    @Test
    void testParseAsLocalDateTimeWithInvalidMinute() {
        assertThrows(DateTimeException.class, ()->DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDDHHMM, "202012251366"));
    }

    @Test
    void testParseAsLocalDateTimeWithGarbageInputString() {
        assertThrows(DateTimeException.class, ()->DateTimeUtils.parseAsLocalDateTime(DateTimeUtils.DTF_YYYYMMDD, "garbage"));
    }

}

谢谢@techtrip。对不起,正如你所指出的,在这两种情况下,我的意思都是2/29。我之所以避免使用
DateTimeFormatter.BASIC\u ISO\u DATE
,是因为它有一个我不想解析的可选部分。我可以做一个没有可选部分的等效格式化程序吗?我修改了答案以考虑到你的最后评论。请看一看!够好了,@techtrip。这也是我发现的。注意:您可以去掉
parsecaseminsensitive()
——不需要,因为yyyyMMdd没有大小写问题;
startopical()
,因为您没有开始任何可选的操作。太棒了,谢谢!这一切都是全新的,文档当然可以稍微好一点,特别是解析器样式和解析器之间的区别。这让我有点糊涂了。而不是
TemporalAccessor ta=dtf.parse(s)
然后
返回LocalDate.from(ta)
您只需执行
返回LocalDate.parse(s,dtf)。多解释一下需要注意的问题可能会有所帮助。你的代码很好。