java.time DateTimeFormatter 使用灵活的回退值进行解析
java.time DateTimeFormatter parsing with flexible fallback values
我正在尝试将一些代码从 joda 时间移植到 java 时间。
JodaTime 可以像这样为年份指定回退值
parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);
无论解析器的外观如何(是否包含年份),都会对其进行解析。
java.time 那里变得更加严格。尽管有 DateTimeFormatterBuilder.parseDefaulting()
方法允许您指定回退,但只有当该特定字段在您要解析的日期中指定为 not 或标记为可选。
如果您对传入的日期格式没有任何控制,因为它是用户提供的,这使得调用 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"));
}
期望的结果:测试应该解析字符串并通过(“绿色”)。
观察结果:测试的最后一行抛出异常,并显示以下消息和堆栈跟踪。
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
我不确定你是否应该想要这个,但我将它作为一个选项提供。
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;
}
我采取相反的方式:我没有采用缺失的字段并将它们调整到已解析的对象中,而是采用默认的 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"));
它打印了预期的
1970-12-06
2018-12-06
2018-12-06
进一步思考
这听起来有点像您的软件是围绕 Joda-Time 的这一特定行为设计的。因此,即使你正在从 Joda 迁移到 java.time
——一个你应该感到高兴的迁移——如果是我,我会考虑在这个特定的角落保留 Joda-Time。这不是最令人愉快的选择,尤其是因为 Joda-time 和 java.time
(据我所知)之间不存在直接转换。你需要自己权衡利弊。
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
}
但如果你想要一个通用的解决方案,恐怕会更复杂。一种替代方法是解析输入并检查其中是否有任何年份字段。如果有 none,那么我们将其更改为 return 年份的默认值:
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;
}
首先我们解析并得到一个包含所有解析字段的TemporalAccessor
。然后我们检查它是否有 year 或 year-of-era 字段。如果它没有任何这些,我们将创建另一个 TemporalAccessor
并使用一些年份的默认值。
在上面的代码中,我使用的是 1970,但您可以将其更改为您需要的任何值。 withYear
方法有一些重要的细节需要注意:
- 我假设输入总是有月和日。如果不是这种情况,您可以更改下面的代码以使用它们的默认值
- 要检查字段是否存在,请使用
isSupported
方法
LocalDate.from
internally uses a TemporalQuery
, which in turn queries the epoch-day field,但是解析出来的对象没有年份的时候,无法计算纪元日,所以我也在计算
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);
}
}
};
}
现在可以了:
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
但我仍然认为第一个解决方案更简单。
备选
假设您总是创建 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;
}
方法 parseBest
receives a list of TemporalQuery
instances (or equivalent method references, as the from
methods above) 并尝试按顺序调用它们:在上面的代码中,首先它尝试创建一个 LocalDate
,如果不可能,请尝试MonthDay
.
然后我检查类型 returned 并采取相应的行动。您可以扩展它以检查任意数量的类型,您也可以编写自己的 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
您可以尝试将我的库 Time4J 的解析引擎作为一种 enhancement/improvement,然后使用以下代码在解析期间生成 java.time.LocalDate
的实例:
static ChronoFormatter<LocalDate> createParser(String pattern) {
return ChronoFormatter // maybe consider caching the immutable formatter per pattern
.ofPattern(
pattern,
PatternType.CLDR,
Locale.ROOT, // locale-sensitive patterns require another locale
PlainDate.axis(TemporalType.LOCAL_DATE) // converts to java.time.LocalDate
)
.withDefault(PlainDate.YEAR, 1970)
.with(Leniency.STRICT);
}
public static void main(String[] args) throws Exception {
System.out.println(createParser("uuuu/MM/dd").parse("2018/12/06")); // 2018-12-06
System.out.println(createParser("yyyy/MM/dd").parse("2018/12/06")); // 2018-12-06
System.out.println(createParser("MM/dd").parse("12/06")); // 1970-12-06
}
这是可行的,因为 - 尽管有严格的解析模式(其中检查了矛盾的元素值 - 模式符号 "y" 将映射到 "u"(proleptic gregorian year)as只要没有纪元符号 "G" 相应的历史纪元元素。
关于替代格式引擎的许多其他功能,请参阅 documentation. A builder for specialized element syntax or customized patterns 也可用。存在定义默认值的其他变体。您的 Joda-default-code 可能会以这种方式迁移(使用系统时区,但也很容易使用 UTC):
parser.withDefaultSupplier( // also works if current year is changing
PlainDate.YEAR,
() -> SystemClock.inLocalView().today().getYear()
// or: () -> SystemClock.inZonalView(ZonalOffset.UTC).getYear()
)
另一个关于使用模式的重要通知
Joda 和 java.time
的模式语法不同。你知道这个事实吗?迁移时无论如何都必须转换模式:
- y => 你
- Y => 是
- x => Y
我正在尝试将一些代码从 joda 时间移植到 java 时间。
JodaTime 可以像这样为年份指定回退值
parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);
无论解析器的外观如何(是否包含年份),都会对其进行解析。
java.time 那里变得更加严格。尽管有 DateTimeFormatterBuilder.parseDefaulting()
方法允许您指定回退,但只有当该特定字段在您要解析的日期中指定为 not 或标记为可选。
如果您对传入的日期格式没有任何控制,因为它是用户提供的,这使得调用 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"));
}
期望的结果:测试应该解析字符串并通过(“绿色”)。
观察结果:测试的最后一行抛出异常,并显示以下消息和堆栈跟踪。
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
我不确定你是否应该想要这个,但我将它作为一个选项提供。
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;
}
我采取相反的方式:我没有采用缺失的字段并将它们调整到已解析的对象中,而是采用默认的 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"));
它打印了预期的
1970-12-06
2018-12-06
2018-12-06
进一步思考
这听起来有点像您的软件是围绕 Joda-Time 的这一特定行为设计的。因此,即使你正在从 Joda 迁移到 java.time
——一个你应该感到高兴的迁移——如果是我,我会考虑在这个特定的角落保留 Joda-Time。这不是最令人愉快的选择,尤其是因为 Joda-time 和 java.time
(据我所知)之间不存在直接转换。你需要自己权衡利弊。
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
}
但如果你想要一个通用的解决方案,恐怕会更复杂。一种替代方法是解析输入并检查其中是否有任何年份字段。如果有 none,那么我们将其更改为 return 年份的默认值:
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;
}
首先我们解析并得到一个包含所有解析字段的TemporalAccessor
。然后我们检查它是否有 year 或 year-of-era 字段。如果它没有任何这些,我们将创建另一个 TemporalAccessor
并使用一些年份的默认值。
在上面的代码中,我使用的是 1970,但您可以将其更改为您需要的任何值。 withYear
方法有一些重要的细节需要注意:
- 我假设输入总是有月和日。如果不是这种情况,您可以更改下面的代码以使用它们的默认值
- 要检查字段是否存在,请使用
isSupported
方法
- 要检查字段是否存在,请使用
LocalDate.from
internally uses aTemporalQuery
, which in turn queries the epoch-day field,但是解析出来的对象没有年份的时候,无法计算纪元日,所以我也在计算
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);
}
}
};
}
现在可以了:
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
但我仍然认为第一个解决方案更简单。
备选
假设您总是创建 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;
}
方法 parseBest
receives a list of TemporalQuery
instances (or equivalent method references, as the from
methods above) 并尝试按顺序调用它们:在上面的代码中,首先它尝试创建一个 LocalDate
,如果不可能,请尝试MonthDay
.
然后我检查类型 returned 并采取相应的行动。您可以扩展它以检查任意数量的类型,您也可以编写自己的 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
您可以尝试将我的库 Time4J 的解析引擎作为一种 enhancement/improvement,然后使用以下代码在解析期间生成 java.time.LocalDate
的实例:
static ChronoFormatter<LocalDate> createParser(String pattern) {
return ChronoFormatter // maybe consider caching the immutable formatter per pattern
.ofPattern(
pattern,
PatternType.CLDR,
Locale.ROOT, // locale-sensitive patterns require another locale
PlainDate.axis(TemporalType.LOCAL_DATE) // converts to java.time.LocalDate
)
.withDefault(PlainDate.YEAR, 1970)
.with(Leniency.STRICT);
}
public static void main(String[] args) throws Exception {
System.out.println(createParser("uuuu/MM/dd").parse("2018/12/06")); // 2018-12-06
System.out.println(createParser("yyyy/MM/dd").parse("2018/12/06")); // 2018-12-06
System.out.println(createParser("MM/dd").parse("12/06")); // 1970-12-06
}
这是可行的,因为 - 尽管有严格的解析模式(其中检查了矛盾的元素值 - 模式符号 "y" 将映射到 "u"(proleptic gregorian year)as只要没有纪元符号 "G" 相应的历史纪元元素。
关于替代格式引擎的许多其他功能,请参阅 documentation. A builder for specialized element syntax or customized patterns 也可用。存在定义默认值的其他变体。您的 Joda-default-code 可能会以这种方式迁移(使用系统时区,但也很容易使用 UTC):
parser.withDefaultSupplier( // also works if current year is changing
PlainDate.YEAR,
() -> SystemClock.inLocalView().today().getYear()
// or: () -> SystemClock.inZonalView(ZonalOffset.UTC).getYear()
)
另一个关于使用模式的重要通知
Joda 和 java.time
的模式语法不同。你知道这个事实吗?迁移时无论如何都必须转换模式:
- y => 你
- Y => 是
- x => Y