现有年份、可选月份和可选日期的 DateTimeFormatter 验证

hoo*_*knc 5 java java-time datetimeformatter

我们正在尝试编写一个 DateTimeFormatter 来帮助我们验证ISO 8601,该 ISO 8601 允许最终用户仅输入年份、年份和月份,或者年份、月份和日期。我们还想验证输入的日期是否确实存在。

在下面的代码中,有两个日期和日期验证表现得很时髦的示例。第一个是仅针对一年和可选月份的验证测试。这不能正确验证月份“00”。

第二个示例显示测试(NOT)因不正确的可选月份值而失败。

任何友好的指示将不胜感激。

import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.TemporalQuery;

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

public class DateTimeFormatterForStackOverflowTest {

    @Test
    public void test_year_or_year_and_month_not_valid() {

        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .appendPattern("uuuu[-MM]")
                .toFormatter()
                .withResolverStyle(ResolverStyle.STRICT);

        this.expectException("1984-0", formatter, YearMonth::from, Year::from);
        // Doesn't throw exception. 
        this.expectException("1984-00", formatter, YearMonth::from, Year::from);
        // Doesn't throw exception.
        this.expectException("1984-13", formatter, YearMonth::from, Year::from);
        // Doesn't throw exception.
        this.expectException("1984-99", formatter, YearMonth::from, Year::from);
    }

    @Test
    public void test_year_or_year_month_or_year_month_day_not_valid() {

        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
                .appendPattern("[uuuu-MM-dd][uuuu-MM][uuuu]")
                .toFormatter()
                .withResolverStyle(ResolverStyle.STRICT);

        this.expectException("1984-0", formatter, LocalDate::from, YearMonth::from, Year::from);
        // Doesn't throw exception.
        this.expectException("1984-00", formatter, LocalDate::from, YearMonth::from, Year::from);
        // Doesn't throw exception.
        this.expectException("1984-13", formatter, LocalDate::from, YearMonth::from, Year::from);
        // Doesn't throw exception.
        this.expectException("1984-99", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-00-01", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-13-01", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-01-0", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-01-00", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-01-32", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-12-00", formatter, LocalDate::from, YearMonth::from, Year::from);
        this.expectException("1984-12-32", formatter, LocalDate::from, YearMonth::from, Year::from);
    }

    private void expectException(String value, DateTimeFormatter formatter, TemporalQuery<?>... queries) {

        assertThrows(DateTimeParseException.class,
                () -> formatter.parseBest(value, queries));
    }
}
Run Code Online (Sandbox Code Playgroud)

测试输出:

org.opentest4j.AssertionFailedError: Expected java.time.format.DateTimeParseException to be thrown, but nothing was thrown.

    at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:73)
    at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35)
    at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3083)
    at DateTimeFormatterForStackOverflowTest.expectException(DateTimeFormatterForStackOverflowTest.java:59)
    at DateTimeFormatterForStackOverflowTest.test_year_or_year_month_or_year_month_day_not_valid(DateTimeFormatterForStackOverflowTest.java:43)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.ForkJoinPoolHierarchicalTestExecutorService$ExclusiveTask.compute(ForkJoinPoolHierarchicalTestExecutorService.java:185)
    at java.base/java.util.concurrent.RecursiveAction.exec(RecursiveAction.java:189)
    at java.base/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:290)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
Run Code Online (Sandbox Code Playgroud)

hoo*_*knc 0

以下是我们最终为解决特定 iso 日期验证问题所做的事情。

首先,我们创建了 5 个不同的验证器(一个对应于我们接受的每个不同的 iso 日期),每个验证器都实现了以下接口:

package app.validation.type;

public interface IsoDateFormatValidator {

    boolean isValid(String date);
}

Run Code Online (Sandbox Code Playgroud)

然后,我们创建了一个验证器,它将检查基于 iso 日期的验证器列表,以查看输入的日期是否有效。

package app.validation.type;

import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;

public class ValidIsoDateFormatValidator implements ConstraintValidator<ValidIsoDateFormat, String> {

    private final List<IsoDateFormatValidator> isoDateFormatValidatorList;

    public ValidIsoDateFormatValidator(List<IsoDateFormatValidator> isoDateFormatValidatorList) {
        this.isoDateFormatValidatorList = isoDateFormatValidatorList;
    }

    @Override
    public void initialize(ValidIsoDateFormat annotation) {

    }

    @Override
    public boolean isValid(String inputDate, ConstraintValidatorContext constraintValidatorContext) {

        boolean valid = true;

        if (StringUtils.hasText(inputDate)) {

            valid = isoDateFormatValidatorList.stream()
                    .map(v -> v.isValid(inputDate))
                    .anyMatch((Boolean.TRUE::equals));
        }

        return valid;
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是我们使用的各个 iso 日期验证器。

日期/时间 YYYY-MM-DD HH:MM:SS

package app.validation.type;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;

public class IsoDateTimeFormatValidator implements IsoDateFormatValidator {

    private final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("uuuu-MM-dd HH:mm:ss")
            .toFormatter()
            .withResolverStyle(ResolverStyle.LENIENT);

    @Override
    public boolean isValid(String date) {

        boolean valid = false;

        try {

            LocalDateTime ldt =  LocalDateTime.parse(date, formatter);
            valid = true;

        } catch (DateTimeParseException ignored) {

        }

        return valid;
    }
}
Run Code Online (Sandbox Code Playgroud)

带毫秒的日期/时间 YYYY-MM-DD HH:MM:SS.SSS

package app.validation.type;

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.TemporalAccessor;

public class IsoDateTimeMillisFormatValidator implements IsoDateFormatValidator {

    private final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("uuuu-MM-dd HH:mm:ss.SSS")
            .toFormatter()
            .withResolverStyle(ResolverStyle.LENIENT);

    @Override
    public boolean isValid(String date) {

        boolean valid = false;

        try {

            TemporalAccessor ta = formatter.parse(date);
            valid = true;

        } catch (DateTimeParseException ignored) {

        }

        return valid;
    }
}
Run Code Online (Sandbox Code Playgroud)

年份 YYYY

package app.validation.type;

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;

public class IsoYearFormatValidator implements IsoDateFormatValidator {

    private final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("uuuu")
            .toFormatter();

    @Override
    public boolean isValid(String date) {

        boolean valid = false;

        try {

            TemporalAccessor ta = formatter.parse(date);
            valid = true;

        } catch (DateTimeParseException ignored) {

        }

        return valid;
    }
}
Run Code Online (Sandbox Code Playgroud)

年/月 YYYY/MM

package app.validation.type;

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;

public class IsoYearMonthFormatValidator implements IsoDateFormatValidator {

    private final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("uuuu-MM")
            .toFormatter();

    @Override
    public boolean isValid(String date) {

        boolean valid = false;

        try {

            TemporalAccessor ta = formatter.parse(date);
            valid = true;

        } catch (DateTimeParseException ignored) {

        }

        return valid;
    }
}
Run Code Online (Sandbox Code Playgroud)

年/月/日 YYYY/MM/DD

package app.validation.type;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;

public class IsoYearMonthDayFormatValidator implements IsoDateFormatValidator {

    private final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendPattern("uuuu-MM-dd")
            .toFormatter()
            .withResolverStyle ( ResolverStyle.LENIENT );

    @Override
    public boolean isValid(String date) {

        boolean valid = false;

        try {

            LocalDate ld = LocalDate.parse (date, formatter);
            valid = true;

        } catch (DateTimeParseException ignored) {

        }

        return valid;
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,如果您有其他 iso 日期/时间所需的验证未在此处列出,则可以添加这些验证,因为ValidIsoDateFormatValidator需要 s 列表IsoDateFormatValidator