我正在尝试将一些代码从joda时间移植到java时间。
JodaTime有可能像这样指定一年的回退值
parser.withDefaultYear((new DateTime(DateTimeZone.UTC)).getYear()).parseDateTime(text);
无论解析器的外观如何(如果包含一年或不包括一年),都将对其进行解析。
java.time在那里变得更加严格。即使有DateTimeFormatterBuilder.parseDefaulting()
方法允许您指定回退,但只有在您要解析或标记为可选日期的特定字段不时,此方法才有效。< / p>
如果您对用户提供的传入日期格式没有任何控制权,那么在拨打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"));
}
期望的结果:测试应解析字符串并传递(“变为绿色”)。
观察到的结果:测试的最后一行抛出了一个带有以下消息和堆栈跟踪的异常。
文字&#39; 2018/12/06&#39;无法解析:发现冲突: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
答案 0 :(得分:4)
我怀疑你是否应该这样做,但我把它作为一种选择。
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
之间没有直接的转换(我知道)。你需要自己权衡利弊。
答案 1 :(得分:4)
parseDefaulting
将设置该字段的值,即使对于不在该模式中的字段也是如此,因此您可能会遇到年份和年份的情况存在于解析结果中。
对我来说,最简单的解决方案是评论中的建议:检查输入是否包含带有正则表达式的年份(或看起来像一个的东西,例如4位数),或检查输入? length,然后相应地创建格式化程序(并且没有默认值)。例子:
if (input_without_year) {
LocalDate d = MonthDay
.parse("12/06", DateTimeFormatter.ofPattern("MM/dd"))
.atYear(1970);
} else {
// use formatter with year, without default values
}
但如果你想要一个通用的解决方案,我担心它会更复杂。另一种方法是解析输入并检查其中是否有任何年份字段。如果没有,那么我们将其更改为返回年份的默认值:
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
。然后我们检查它是否有年份或年份字段。如果它没有任何一个,我们创建另一个TemporalAccessor
,其中包含一年的默认值。
在上面的代码中,我使用1970,但您可以将其更改为您需要的任何内容。 withYear
方法有一些重要的细节需要注意:
isSupported
方法LocalDate.from
internally uses a TemporalQuery
,反过来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
。
然后我检查返回的类型并采取相应的行动。您可以展开此选项以检查所需的类型,也可以编写自己的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
答案 2 :(得分:1)
您可以尝试我的lib Time4J的解析引擎作为一种增强/改进,然后使用以下代码在解析过程中生成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
}
这是因为 - 尽管有严格的解析模式(检查矛盾元素值 - 模式符号&#34; y&#34;将映射到&#34; u&#34;(预感格里高利年)只要没有时代符号&#34; G&#34;各自的历史时代元素。
关于替代格式引擎的许多其他功能,请参阅documentation。还可以使用专用元素语法或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
的模式语法不同。你知道这个事实吗?迁移时,您还必须转换模式: