如果您只是解析String
输入,那么它很简单:
LocalDate d1 = LocalDate.parse("1893-04-01");
System.out.println(d1); // 1893-04-01
LocalDate d2 = LocalDate.parse("1400-04-01");
System.out.println(d2); // 1400-04-01
输出是:
1893-04-01
1400-04-01
但是如果你有一个java.util.Date
对象并且需要转换它,那就有点复杂了。
Ajava.util.Date
包含从 unix epoch ( 1970-01-01T00:00Z
) 开始的毫秒数。因此,您可以说“它采用 UTC”,但是当您打印它时,该值将“转换”为系统的默认时区(在您的情况下,它是CET
)。并且SimpleDateFormat
还在内部使用默认时区(以我必须承认我不完全理解的晦涩方式)。
在您的示例中,毫秒值-2422054800000
等于 UTC 时刻1893-03-31T23:00:00Z
。在时区检查此值Europe/Berlin
:
System.out.println(Instant.ofEpochMilli(-2422054800000L).atZone(ZoneId.of("Europe/Berlin")));
输出是:
1893-03-31T23:53:28+00:53:28[欧洲/柏林]
是的,这很奇怪,但是在 1900 年之前,所有地方都使用了奇怪的偏移量——在 UTC 标准出现之前,每个城市都有自己的当地时间。这就解释了为什么你得到1893-03-31
. 该Date
对象打印April 1st
可能是因为旧 API ( java.util.TimeZone
) 没有所有偏移历史记录,所以它假定它是+01:00
.
使这项工作的一种替代方法是始终使用UTC作为时区:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // set UTC to the format
Date date = sdf.parse("1893-04-01");
LocalDate d = date.toInstant().atZone(ZoneOffset.UTC).toLocalDate();
System.out.println(d); // 1893-04-01
这将获得正确的本地日期:1893-04-01
.
但是对于之前1582-10-15
的日期,上面的代码不起作用。那是引入公历的日期。在此之前,使用的是儒略历,之前的日期需要调整。
我可以使用ThreeTen Extra 项目(java.time
类的扩展,由同一个人 BTW 创建)来做到这一点。在org.threeten.extra.chrono
包中有JulianChronology
和JulianDate
类:
// using the same SimpleDateFormat as above (with UTC set)
date = sdf.parse("1400-04-01");
// get julian date from date
JulianDate julianDate = JulianChronology.INSTANCE.date(date.toInstant().atZone(ZoneOffset.UTC));
System.out.println(julianDate); // Julian AD 1400-04-01
输出将是:
朱利安公元 1400-04-01
现在我们需要将 转换JulianDate
为LocalDate
. 如果我这样做LocalDate.from(julianDate)
,它会转换为公历(结果是1400-04-10
)。
但是如果你想创建一个LocalDate
with exact 1400-04-01
,你必须这样做:
LocalDate converted = LocalDate.of(julianDate.get(ChronoField.YEAR_OF_ERA),
julianDate.get(ChronoField.MONTH_OF_YEAR),
julianDate.get(ChronoField.DAY_OF_MONTH));
System.out.println(converted); // 1400-04-01
输出将是:
1400-04-01
请注意,之前的日期1582-10-15
有此调整,SimpleDateFormat
无法正确处理这些情况。如果您只需要使用1400-04-01
(年/月/日值),请使用LocalDate
. 但如果您需要将其转换为 a java.util.Date
,请注意它可能不是同一日期(由于公历/朱利安调整)。
如果您不想添加其他依赖项,您也可以手动完成所有数学运算。我已经改编了 ThreeTen 的代码,但 IMO 理想的是使用 API 本身(因为它可以涵盖极端情况和其他我可能通过复制一段代码而丢失的东西):
// auxiliary method
public LocalDate ofYearDay(int prolepticYear, int dayOfYear) {
boolean leap = (prolepticYear % 4) == 0;
if (dayOfYear == 366 && leap == false) {
throw new DateTimeException("Invalid date 'DayOfYear 366' as '" + prolepticYear + "' is not a leap year");
}
Month moy = Month.of((dayOfYear - 1) / 31 + 1);
int monthEnd = moy.firstDayOfYear(leap) + moy.length(leap) - 1;
if (dayOfYear > monthEnd) {
moy = moy.plus(1);
}
int dom = dayOfYear - moy.firstDayOfYear(leap) + 1;
return LocalDate.of(prolepticYear, moy.getValue(), dom);
}
// sdf with UTC set, as above
Date date = sdf.parse("1400-04-01");
ZonedDateTime z = date.toInstant().atZone(ZoneOffset.UTC);
LocalDate d;
// difference between the ISO and Julian epoch day count
long julianToIso = 719164;
int daysPerCicle = (365 * 4) + 1;
long julianEpochDay = z.toLocalDate().toEpochDay() + julianToIso;
long cycle = Math.floorDiv(julianEpochDay, daysPerCicle);
long daysInCycle = Math.floorMod(julianEpochDay, daysPerCicle);
if (daysInCycle == daysPerCicle - 1) {
int year = (int) ((cycle * 4 + 3) + 1);
d = ofYearDay(year, 366);
} else {
int year = (int) ((cycle * 4 + daysInCycle / 365) + 1);
int doy = (int) ((daysInCycle % 365) + 1);
d = ofYearDay(year, doy);
}
System.out.println(d); // 1400-04-01
输出将是:
1400-04-01
只是提醒一下,之后的日期不需要所有这些数学1582-10-15
。
无论如何,如果您有输入String
并想要解析它,请不要使用SimpleDateFormat
- 您可以使用LocalDate.parse()
。或者LocalDate.of(year, month, day)
,如果您已经知道这些值。
但是将这些本地日期从/转换为 ajava.util.Date
更加复杂,因为Date
代表完整的时间戳毫秒,并且日期可能会根据使用的日历系统而有所不同。