alr*_*alr 5 java time datetime datetime-parsing java-time
我正在尝试将一些代码从joda时间移植到Java时间。
JodaTime可以这样指定当年的后备值
parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);
Run Code Online (Sandbox Code Playgroud)
不管解析器的外观如何(如果包含一年或不包含一年),都将对其进行解析。
那里的java.time变得更加严格。即使有DateTimeFormatterBuilder.parseDefaulting()允许您指定后备方法的方法,也只有在您要解析的日期中未指定该特定字段或将其标记为可选时,该方法才有效。
如果您对传入的日期格式没有任何控制权(由用户提供),则这将使调用时变得非常困难parseDefaulting。
是否有任何解决方法,可以在其中指定通用回退日期(如果未指定其值,则格式化程序将使用其值)之类的东西,或者在格式化程序中指定后,如何配置未使用的回退值?
以下是最小,完整和可验证的示例。
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"));
}
Run Code Online (Sandbox Code Playgroud)
所需的结果:测试应解析字符串并通过(“呈绿色”)。
观察到的结果:测试的最后一行引发异常,并显示以下消息和堆栈跟踪。
文本'2018/12/06'无法解析:发现冲突:1970年与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
Run Code Online (Sandbox Code Playgroud)
parseDefaulting 如果未找到该字段的值,即使对于不在模式中的字段,也将设置该字段的值,因此您最终可能会遇到解析结果中同时存在年份和年份的情况。
对我来说,最简单的解决方案是评论中建议的:使用正则表达式检查输入是否包含年份(或看起来像一个的东西,例如 4 位数字),或者通过检查输入的长度,然后创建格式化程序相应地(并且没有默认值)。例子:
if (input_without_year) {
LocalDate d = MonthDay
.parse("12/06", DateTimeFormatter.ofPattern("MM/dd"))
.atYear(1970);
} else {
// use formatter with year, without default values
}
Run Code Online (Sandbox Code Playgroud)
但是如果你想要一个通用的解决方案,恐怕它会更复杂。一种替代方法是解析输入并检查其中是否有任何年份字段。如果没有,那么我们将其更改为返回年份的默认值:
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;
}
Run Code Online (Sandbox Code Playgroud)
首先我们解析并得到一个TemporalAccessor包含所有解析字段的。然后我们检查它是否有 year 或 year-of-era 字段。如果它没有其中任何一个,我们将创建另一个TemporalAccessor具有某些默认值的年份。
在上面的代码中,我使用的是 1970,但您可以将其更改为您需要的任何内容。该withYear方法有一些重要的细节需要注意:
isSupported方法LocalDate.from 内部使用 aTemporalQuery,它反过来查询纪元日字段,但是当解析的对象没有年份时,它无法计算纪元日,所以我也在计算它该withYear方法如下:
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);
}
}
};
}
Run Code Online (Sandbox Code Playgroud)
现在这有效:
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
Run Code Online (Sandbox Code Playgroud)
但我仍然相信第一个解决方案更简单。
假设您总是在创建LocalDate,另一种选择是使用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;
}
Run Code Online (Sandbox Code Playgroud)
该方法parseBest 接收一个TemporalQuery实例列表(或等效的方法引用,如from上面的方法)并尝试按顺序调用它们:在上面的代码中,它首先尝试创建一个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-06
System.out.println(parseLocalDate("yyyy/MM/dd", "2018/12/06")); // 2018-12-06
Run Code Online (Sandbox Code Playgroud)
我不确定您是否应该这样做,但我将其作为一种选择。
private static LocalDate defaults = LocalDate.of(1970, Month.JANUARY, 1);
private static LocalDate parseWithDefaults(String pattern, String dateString) {
TemporalAccessor parsed
= DateTimeFormatter.ofPattern(pattern, Locale.ROOT).parse(dateString);
LocalDate result = defaults;
for (TemporalField field : ChronoField.values()) {
if (parsed.isSupported(field) && result.isSupported(field)) {
result = result.with(field, parsed.getLong(field));
}
}
return result;
}
Run Code Online (Sandbox Code Playgroud)
我采用相反的方法:不是采用丢失的字段并将其调整为解析的对象,而是采用默认LocalDate对象并将其解析的字段调整为该对象。关于此工作原理的规则很复杂,所以恐怕我们可能会感到惊讶或惊讶。另外,在完全指定的日期(如2018/12/06)中,它使用13个字段,因此显然存在一些冗余。但是,我用您的三个测试示例进行了尝试:
System.out.println(parseWithDefaults("MM/dd", "12/06"));
System.out.println(parseWithDefaults("uuuu/MM/dd", "2018/12/06"));
System.out.println(parseWithDefaults("yyyy/MM/dd", "2018/12/06"));
Run Code Online (Sandbox Code Playgroud)
它打印了预期的
1970-12-06
2018-12-06
2018-12-06
Run Code Online (Sandbox Code Playgroud)
进一步思考
听起来这部分软件是根据Joda-Time的这种特定行为设计的。因此,即使您正在从Joda迁移到java.time–您应该很高兴的迁移–如果是我,我还是考虑保留Joda-Time在这个特定角落。这不是最令人愉快的选择,尤其是因为Joda-time和java.time(据我所知)之间不存在直接转换,因此尤其如此。您将需要自己权衡利弊。
| 归档时间: |
|
| 查看次数: |
1507 次 |
| 最近记录: |