具有灵活后备值的java.time DateTimeFormatter解析 [英] java.time DateTimeFormatter parsing with flexible fallback values

查看:86
本文介绍了具有灵活后备值的java.time DateTimeFormatter解析的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试将一些代码从joda time移植到java time。



JodaTime可以为此类年份指定后备值

  parser.withDefaultYear((new DateTime(DateTimeZone.UTC))。getYear())。parseDateTime(text); 

无论解析器的外观如何(如果包含一年或一年),都将对其进行解析。 / p>

java.time在这里变得更加严格。即使存在 DateTimeFormatterBuilder.parseDefaulting()方法,该方法允许您指定后备,但只有在中指定了该特定字段时,此方法才有效要解析的日期或标记为可选的日期。



如果您对传入的日期格式没有任何控制,因为它是用户提供的,那么它将成为超级格式何时调用 parseDefaulting 很困难。



有没有解决方法,可以在其中指定通用后备日期,如果未指定其值,或者格式化程序中指定了它们的值,则格式化程序将使用它们的值?



最小,下面是完整且可验证的示例。

 公共静态DateTimeFormatter ofPattern(字符串模式){
返回new DateTimeFormatterBuilder()
.appendPattern(pattern)
.parseDefaulting(ChronoField.YEAR,197 0)
.toFormatter(Locale.ROOT);
}

public void testPatterns(){
//有效
assertThat(LocalDate.from(ofPattern( MM / dd)。parse( 12 / 06))。toString(),是( 1970-12-06)));
assertThat(LocalDate.from(ofPattern( uuuu / MM / dd)。parse( 2018/12/06))。toString(),is( 2018-12-06)));
//失败,异常,因为它使用了时代
assertThat(LocalDate.from(ofPattern( yyyy / MM / dd)。parse( 2018/12/06))。toString (),is( 2018-12-06)));
}

所需结果:测试应解析字符串并通过(绿色 )。



观察到的结果:测试的最后一行抛出异常,并带有以下消息和堆栈跟踪。



< blockquote>

文本'2018/12/06'无法解析:发现冲突:1970年
与2018年不同




 线程主中的异常java.time.format.DateTimeParseException:无法解析文本 2018/12/06:发现冲突:1970年不同从2018年开始
在java.base / java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1959)
在java.base / java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java :1820)com.ajax.mypackage.MyTest.testPatterns(MyTest.java:33)
在$ com $ .com中引起:java.time.DateTimeException:发现冲突:1970年与2018年不同
在java.base / java.time.chrono.AbstractChronology.addFieldValue(Abs tractChronology.java:676)
在java.base / java.time.chrono.IsoChronology.resolveYearOfEra(IsoChronology.java:620)
在java.base / java.time.chrono.IsoChronology.resolveYearOfEra( IsoChronology.java:126)
在java.base / java.time.chrono.AbstractChronology.resolveDate(AbstractChronology.java:463)
在java.base / java.time.chrono.IsoChronology.resolveDate( IsoChronology.java:585)
在java.base / java.time.chrono.IsoChronology.resolveDate(IsoChronology.java:126)
在java.base / java.time.format.Parsed.resolveDateFields( Parsed.java:360)
(位于java.base / java.time.format.Parsed.resolveFields(Parsed.java:266)
位于java.base / java.time.format.Parsed.resolve(在java.base / java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331)中解析的java.253:
在java.base / java.time.format.DateTimeFormatter.parseResolved0( DateTimeFormatter.java:1994)
,位于java.base / java.time.format.DateTimeFormatter.parse(DateTimeFormat ter.java:1816)
...还有1个


解决方案

parseDefaulting 将设置未找到的字段的值,即使对于不在模式中的字段也是如此,因此您可能会遇到两种情况:



对我来说,最简单的解决方案如注释中所示:检查输入是否包含年份(或带有正则表达式的看起来像一个的东西(例如4位数字),或者检查输入的长度,然后相应地创建格式器(没有默认值)。示例:

  if(input_without_year){
LocalDate d = MonthDay
.parse( 12/06 ,DateTimeFormatter.ofPattern( MM / dd))
.atYear(1970);
} else {
//使用带年份的格式化程序,不带默认值
}

但是,如果您想要通用的解决方案,恐怕它会更复杂。一种选择是解析输入并检查其中是否有年份字段。如果没有,则将其更改为返回年份的默认值:

  public static TemporalAccessor parse(String pattern,String输入){
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern,Locale.ROOT);
最终TemporalAccessor解析= fmt.parse(input);
//检查年份和年份
boolean hasYear = parsed.isSupported(ChronoField.YEAR);
boolean hasYearEra = parsed.isSupported(ChronoField.YEAR_OF_ERA);
if(!hasYear&&!hasYearEra){
//解析的值没有任何年份字段
//返回另一个TemporalAccessor,其默认值为Year
/ /使用1970年-将其更改为Year.now()。getValue(),以获取当年
return withYear(parsed,1970); //在
下方查看此方法的代码}
返回已解析;
}

首先,我们解析并获得 TemporalAccessor 包含所有已解析的字段。然后,我们检查它是否具有年份或年份字段。如果没有任何这些,我们将创建另一个 TemporalAccessor ,并为其设置一些默认值。



在上面的代码,我使用的是1970,但是您可以将其更改为所需的任何值。 withYear 方法有一些重要的细节需要注意:




  • 我假设输入始终有月和日。如果不是这种情况,可以更改下面的代码以为其使用默认值


    • 检查是否存在字段,请使用 isSupported 方法


  • LocalDate.from 内部使用 TemporalQuery ,而该查询时代字段,但是当解析的对象没有年份时,它就无法计算历元日,因此我也在计算它。



withYear 的方法如下:

  public静态TemporalAccessor withYear(TemporalAccessor t,长年){
return new TemporalAccessor(){

@Override
public boolean isSupported(TemporalField field){
//纪元日期由LocalDate.from $ b使用$ b if(field == ChronoField.YEAR_OF_ERA || field == ChronoField.EPOCH_DAY){
返回true;
} else {
return t.isSupported(field);
}
}

@Override
public long getLong(TemporalField字段){
if(field == ChronoField.YEAR_OF_ERA){
返回年份
//纪元日期由LocalDate.from
}否则,如果(field == ChronoField.EPOCH_DAY){
//假设输入始终有月和日
//如果不是这种情况,您也可以更改代码以使用默认值
//并使用MonthDay.of(month,day)
返回MonthDay.from(t).atYear((int )year).toEpochDay();
} else {
return t.getLong(field);
}
}
};
}

现在可以使用了:

  System.out.println(LocalDate.from(parse( MM / dd, 12/06)))); // 1970-12-06 
System.out.println(LocalDate.from(parse( uuuu / MM / dd, 2018/12/06)));; // 2018年12月6日
System.out.println(LocalDate.from(parse( yyyy / MM / dd, 2018/12/06)))); // 2018-12-06

但我仍然相信第一个解决方案会更简单。



替代



假设您一直在创建 LocalDate ,另一种选择是使用 parseBest

  public static LocalDate parseLocalDate(String模式,字符串输入){
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern,Locale.ROOT);

//尝试首先创建LocalDate
//如果可能,尝试创建MonthDay
TemporalAccessor解析= fmt.parseBest(input,LocalDate :: from,MonthDay ::从);

LocalDate dt = null;

//检查解析器
创建的是哪种类型,如果(解析后的LocalDate实例){
dt =(LocalDate)解析;
} else if(已解析的instanceof MonthDay){
//使用1970年-将其更改为Year.now()。getValue(),将当年
dt =(((MonthDay))进行解析) .atYear(1970);
} //其他等等...-做所有可能的情况所需的检查

return dt;
}

方法 parseBest 接收 TemporalQuery 实例的列表(或等效的方法引用,如来自 >上面的方法),并尝试按顺序调用它们:在上面的代码中,它首先尝试创建 LocalDate ,如果不可能,请尝试 MonthDay



然后我检查返回的类型并采取相应措施。您可以扩展它以检查所需的多种类型,还可以编写自己的 TemporalQuery 处理特定情况。



所有情况也都适用:

  System.out.println(parseLocalDate( MM / dd, 12/06)) ; // 1970-12-06 
System.out.println(parseLocalDate( uuuu / MM / dd, 2018/12/06)); // 2018年12月6日
System.out.println(parseLocalDate( yyyy / MM / dd, 2018/12/06)); // 2018-12-06


I am trying to port some code from joda time to java time.

JodaTime had the possibility to specify a fallback value for the year like this

parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);

Regardless how the parser looks (if it includes a year or not), this will be parsed.

java.time has become much more stricter there. Even though there is the DateTimeFormatterBuilder.parseDefaulting() method, which allows you to specify fallbacks, this only works if that specific field is not specified in the date your want to parse or is marked as optional.

If you do not have any control about the incoming date format, as it is user supplied, this makes it super hard when to call parseDefaulting.

Is there any workaround, where I can specify something like either a generic fallback date, whose values get used by the formatter, if they are not specified or how I configure fallback values that are simply not used, when they are specified in the formatter?

Minimal, complete and verifiable example follows.

public static DateTimeFormatter ofPattern(String pattern) {
    return new DateTimeFormatterBuilder()
        .appendPattern(pattern)
        .parseDefaulting(ChronoField.YEAR, 1970)
        .toFormatter(Locale.ROOT);
}

public void testPatterns() {
    // works
    assertThat(LocalDate.from(ofPattern("MM/dd").parse("12/06")).toString(), is("1970-12-06"));
    assertThat(LocalDate.from(ofPattern("uuuu/MM/dd").parse("2018/12/06")).toString(), is("2018-12-06"));
    // fails with exception, as it uses year of era
    assertThat(LocalDate.from(ofPattern("yyyy/MM/dd").parse("2018/12/06")).toString(), is("2018-12-06"));
}

Desired result: The test should parse the strings and pass ("be green").

Observed result: The last line of the test throws an exception with the following message and stack trace.

Text '2018/12/06' could not be parsed: Conflict found: Year 1970 differs from Year 2018

Exception in thread "main" java.time.format.DateTimeParseException: Text '2018/12/06' could not be parsed: Conflict found: Year 1970 differs from Year 2018
    at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1959)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1820)
    at com.ajax.mypackage.MyTest.testPatterns(MyTest.java:33)
Caused by: java.time.DateTimeException: Conflict found: Year 1970 differs from Year 2018
    at java.base/java.time.chrono.AbstractChronology.addFieldValue(AbstractChronology.java:676)
    at java.base/java.time.chrono.IsoChronology.resolveYearOfEra(IsoChronology.java:620)
    at java.base/java.time.chrono.IsoChronology.resolveYearOfEra(IsoChronology.java:126)
    at java.base/java.time.chrono.AbstractChronology.resolveDate(AbstractChronology.java:463)
    at java.base/java.time.chrono.IsoChronology.resolveDate(IsoChronology.java:585)
    at java.base/java.time.chrono.IsoChronology.resolveDate(IsoChronology.java:126)
    at java.base/java.time.format.Parsed.resolveDateFields(Parsed.java:360)
    at java.base/java.time.format.Parsed.resolveFields(Parsed.java:266)
    at java.base/java.time.format.Parsed.resolve(Parsed.java:253)
    at java.base/java.time.format.DateTimeParseContext.toResolved(DateTimeParseContext.java:331)
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1994)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1816)
    ... 1 more

解决方案

parseDefaulting will set the value of the field if it's not found, even for fields that are not in the pattern, so you may end up with situations where both year and year-of-era are present in the parsed result.

To me, the easiest solution would be as suggested in the comments: check if the input contains a year (or something that looks like one, such as 4 digits) with a regex, or by checking the input's length, and then create the formatter accordingly (and without default values). Examples:

if (input_without_year) {
    LocalDate d = MonthDay
                      .parse("12/06", DateTimeFormatter.ofPattern("MM/dd"))
                      .atYear(1970);
} else {
    // use formatter with year, without default values
}

But if you want a generic solution, I'm afraid it's more complicated. One alternative is to parse the input and check if there are any year field in it. If there's none, then we change it to return a default value for the year:

public static TemporalAccessor parse(String pattern, String input) {
    DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.ROOT);
    final TemporalAccessor parsed = fmt.parse(input);
    // check year and year of era
    boolean hasYear = parsed.isSupported(ChronoField.YEAR);
    boolean hasYearEra = parsed.isSupported(ChronoField.YEAR_OF_ERA);
    if (!hasYear && !hasYearEra) {
        // parsed value doesn't have any year field
        // return another TemporalAccessor with default value for year
        // using year 1970 - change it to Year.now().getValue() for current year
        return withYear(parsed, 1970); // see this method's code below
    }
    return parsed;
}

First we parse and get a TemporalAccessor containing all the parsed fields. Then we check if it has year or year-of-era field. If it doesn't have any of those, we create another TemporalAccessor with some default value for year.

In the code above, I'm using 1970, but you can change it to whatever you need. The withYear method has some important details to notice:

  • I'm assuming that the input always has month and day. If it's not the case, you can change the code below to use default values for them
    • to check if a field is present, use the isSupported method
  • LocalDate.from internally uses a TemporalQuery, which in turn queries the epoch-day field, but when the parsed object doesn't have the year, it can't calculate the epoch-day, so I'm calculating it as well

The withYear method is as follows:

public static TemporalAccessor withYear(TemporalAccessor t, long year) {
    return new TemporalAccessor() {

        @Override
        public boolean isSupported(TemporalField field) {
            // epoch day is used by LocalDate.from
            if (field == ChronoField.YEAR_OF_ERA || field == ChronoField.EPOCH_DAY) {
                return true;
            } else {
                return t.isSupported(field);
            }
        }

        @Override
        public long getLong(TemporalField field) {
            if (field == ChronoField.YEAR_OF_ERA) {
                return year;
                // epoch day is used by LocalDate.from
            } else if (field == ChronoField.EPOCH_DAY) {
                // Assuming the input always have month and day
                // If that's not the case, you can change the code to use default values as well,
                // and use MonthDay.of(month, day)
                return MonthDay.from(t).atYear((int) year).toEpochDay();
            } else {
                return t.getLong(field);
            }
        }
    };
}

Now this works:

System.out.println(LocalDate.from(parse("MM/dd", "12/06"))); // 1970-12-06
System.out.println(LocalDate.from(parse("uuuu/MM/dd", "2018/12/06"))); // 2018-12-06
System.out.println(LocalDate.from(parse("yyyy/MM/dd", "2018/12/06"))); // 2018-12-06

But I still believe the first solution is simpler.

Alternative

Assuming that you're always creating a LocalDate, another alternative is to use parseBest:

public static LocalDate parseLocalDate(String pattern, String input) {
    DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.ROOT);

    // try to create a LocalDate first
    // if not possible, try to create a MonthDay
    TemporalAccessor parsed = fmt.parseBest(input, LocalDate::from, MonthDay::from);

    LocalDate dt = null;

    // check which type was created by the parser
    if (parsed instanceof LocalDate) {
        dt = (LocalDate) parsed;
    } else if (parsed instanceof MonthDay) {
        // using year 1970 - change it to Year.now().getValue() for current year
        dt = ((MonthDay) parsed).atYear(1970);
    } // else etc... - do as many checkings you need to handle all possible cases

    return dt;
}

The method parseBest receives a list of TemporalQuery instances (or equivalent method references, as the from methods above) and try to call them in order: in the code above, first it tries to create a LocalDate, and if it's not possible, try a MonthDay.

Then I check the type returned and act accordingly. You can expand this to check as many types you want, and you can also write your own TemporalQuery to handle specific cases.

With this, all cases also work:

System.out.println(parseLocalDate("MM/dd", "12/06")); // 1970-12-06
System.out.println(parseLocalDate("uuuu/MM/dd", "2018/12/06")); // 2018-12-06
System.out.println(parseLocalDate("yyyy/MM/dd", "2018/12/06")); // 2018-12-06

这篇关于具有灵活后备值的java.time DateTimeFormatter解析的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆