如何在 java 13 或更高版本中格式化 OffsetDateTime?
How to format OffsetDateTime in java 13 or above?
在Java11中,时钟系统使用毫秒精度,但显然在Java13及更高版本中,它使用微秒精度,这导致我的测试失败。例如,OffsetDateTime.now()
给我这个日期“2021-12-10T10:58:05.309594500+01:00”,而当我从数据库“2021-12-10T10:58:05.309595+01:00”中读取这个日期时”。我正在寻找一种方法来格式化第一个日期,使它们应该相等。我确实希望使用 OffsetDateTime 类型而不是字符串。
更新:
我已经意识到当我将 java 版本从 11 升级到 17 而不是在本地时出现了这个问题,当 gitlab 运行测试时我遇到了这个问题。
这是测试:
@Test
fun `can store, find and delete a failed-message`() {
// given: a failed-message
val failedMessage = FailedMessage(
failedMessageId = FailedMessageId("the-subcription", "the-message-id"),
messageAttributes = mapOf("one" to "een", "two" to "twee"),
messagePayload = "message-payload",
exception = "exception",
dateTime = OffsetDateTime.now(),
stackTrace = "stackey tracey"
)
failedMessageRepository.store(failedMessage)
assertEquals(failedMessage, failedMessageRepository.find(failedMessage.failedMessageId))
}
由于日期时间不相等,此测试失败。这是日志:
<FailedMessage(failedMessageId=the-subcription-the-message-id, messageAttributes={one=een, two=twee}, messagePayload=message-payload, exception=exception, dateTime=2021-12-10T10:58:05.309594500+01:00, stackTrace=stackey tracey)>
but was:
<FailedMessage(failedMessageId=the-subcription-the-message-id, messageAttributes={one=een, two=twee}, messagePayload=message-payload, exception=exception, dateTime=2021-12-10T10:58:05.309595+01:00, stackTrace=stackey tracey)>
你能帮帮我吗?
OffsetDateTime
总是 具有纳秒精度。它的 now
方法可能没有,具体取决于平台和 Java 版本。对象内部有。所以你不可能有一个 OffsetDateTime
秒数少于 9 位小数。
打印了多少位小数是另一个问题。打印 OffsetDateTime
时真正打印的是 String
。如果您只是打印对象,它的 toString
方法会被隐式调用以生成我们在打印中看到的字符串。 OffsetDateTime.toString()
始终为我们提供 尽可能多的 3 位小数组 以呈现完整精度。因此,如果小数是 .100000000
,则打印 .100
。如果它们是 .309594500
,正如您已经看到的那样,所有 9 位小数都出现在字符串中。您无法更改此行为。
如果你想打印不同的字符串,你可以使用 DateTimeFormatter
和你喜欢的小数位数。
那么你可以做什么:
- 您可以修改
OffsetDateTime
,或者实际上,您可以像旧的一样创建一个新的,只是更多的小数点为零,这反过来可能会导致 [=27= 打印更少的小数点].
- 您可以将
OffsetDateTime
格式化为您喜欢的字符串。
以下代码片段结合了这两个选项。为了仅格式化小数点到最后一个非零小数点,我使用以下格式化程序:
private static final DateTimeFormatter ODT_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("uuuu-MM-dd'T'HH:mm.ss")
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.appendOffsetId()
.toFormatter(Locale.ROOT);
示范:
OffsetDateTime dateTime = OffsetDateTime.parse("2021-12-10T10:58:05.309594500+01:00");
int nanos = dateTime.getNano();
OffsetDateTime with7Decimals = dateTime.withNano(nanos / 100 * 100);
System.out.println("toString(): " + with7Decimals);
System.out.println("Formatted: " + with7Decimals.format(ODT_FORMATTER));
OffsetDateTime with5Decimals = dateTime.withNano(nanos / 10000 * 10000);
System.out.println("toString(): " + with5Decimals);
System.out.println("Formatted: " + with5Decimals.format(ODT_FORMATTER));
输出:
toString(): 2021-12-10T10:58:05.309594500+01:00
Formatted: 2021-12-10T10:58.05.3095945+01:00
toString(): 2021-12-10T10:58:05.309590+01:00
Formatted: 2021-12-10T10:58.05.30959+01:00
为您测试 我认为没有理由使用字符串进行比较。在将这些小数设置为零之后,只需比较 OffsetDateTime
对象,这些小数无论如何都会在您的数据库中丢失。
编辑: 测试失败的解决方法?下一行是您获取当前时间的地方,其精度比数据库可以存储的精度更高。
dateTime = OffsetDateTime.now(),
因此将其更改为仅提供您的数据库支持的精度:
dateTime = OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS),
这应该会导致保存和检索时间而无需进一步舍入。
为什么会出现这个问题? OffsetDateTime.now()
在不同的平台和Java版本上精度不同。我不知道 Java 11 和 13 在任何平台上有什么区别,但没有理由不应该有一个,而且在某个即将发布的版本中可能会有另一个。此外,如果您在 Linux、Windows 和 Mac 之间移动,即使使用相同的 Java 版本,您也可能会遇到差异。问题的另一部分是数据库的精度有限,或者更准确地说,是数据库中用于存储时间的数据类型。如果您使用的是 timestamp with time zone
,则您可能拥有数据库可以提供的最佳精度。显然,您的数据库具有微秒精度(秒数为 6 位小数),而您的 Java 13 OffsetDateTime.now()
具有至少半微秒(500 纳秒)的精度。所以你从 Java 13 开始得到一个数据库无法存储的值。显然,您的数据库或数据库驱动程序随后会根据您的情况将值四舍五入。这导致从数据库中检索回的值与您尝试保存的值不完全相同。
在Java11中,时钟系统使用毫秒精度,但显然在Java13及更高版本中,它使用微秒精度,这导致我的测试失败。例如,OffsetDateTime.now()
给我这个日期“2021-12-10T10:58:05.309594500+01:00”,而当我从数据库“2021-12-10T10:58:05.309595+01:00”中读取这个日期时”。我正在寻找一种方法来格式化第一个日期,使它们应该相等。我确实希望使用 OffsetDateTime 类型而不是字符串。
更新: 我已经意识到当我将 java 版本从 11 升级到 17 而不是在本地时出现了这个问题,当 gitlab 运行测试时我遇到了这个问题。
这是测试:
@Test
fun `can store, find and delete a failed-message`() {
// given: a failed-message
val failedMessage = FailedMessage(
failedMessageId = FailedMessageId("the-subcription", "the-message-id"),
messageAttributes = mapOf("one" to "een", "two" to "twee"),
messagePayload = "message-payload",
exception = "exception",
dateTime = OffsetDateTime.now(),
stackTrace = "stackey tracey"
)
failedMessageRepository.store(failedMessage)
assertEquals(failedMessage, failedMessageRepository.find(failedMessage.failedMessageId))
}
由于日期时间不相等,此测试失败。这是日志:
<FailedMessage(failedMessageId=the-subcription-the-message-id, messageAttributes={one=een, two=twee}, messagePayload=message-payload, exception=exception, dateTime=2021-12-10T10:58:05.309594500+01:00, stackTrace=stackey tracey)>
but was:
<FailedMessage(failedMessageId=the-subcription-the-message-id, messageAttributes={one=een, two=twee}, messagePayload=message-payload, exception=exception, dateTime=2021-12-10T10:58:05.309595+01:00, stackTrace=stackey tracey)>
你能帮帮我吗?
OffsetDateTime
总是 具有纳秒精度。它的 now
方法可能没有,具体取决于平台和 Java 版本。对象内部有。所以你不可能有一个 OffsetDateTime
秒数少于 9 位小数。
打印了多少位小数是另一个问题。打印 OffsetDateTime
时真正打印的是 String
。如果您只是打印对象,它的 toString
方法会被隐式调用以生成我们在打印中看到的字符串。 OffsetDateTime.toString()
始终为我们提供 尽可能多的 3 位小数组 以呈现完整精度。因此,如果小数是 .100000000
,则打印 .100
。如果它们是 .309594500
,正如您已经看到的那样,所有 9 位小数都出现在字符串中。您无法更改此行为。
如果你想打印不同的字符串,你可以使用 DateTimeFormatter
和你喜欢的小数位数。
那么你可以做什么:
- 您可以修改
OffsetDateTime
,或者实际上,您可以像旧的一样创建一个新的,只是更多的小数点为零,这反过来可能会导致 [=27= 打印更少的小数点]. - 您可以将
OffsetDateTime
格式化为您喜欢的字符串。
以下代码片段结合了这两个选项。为了仅格式化小数点到最后一个非零小数点,我使用以下格式化程序:
private static final DateTimeFormatter ODT_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("uuuu-MM-dd'T'HH:mm.ss")
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.appendOffsetId()
.toFormatter(Locale.ROOT);
示范:
OffsetDateTime dateTime = OffsetDateTime.parse("2021-12-10T10:58:05.309594500+01:00");
int nanos = dateTime.getNano();
OffsetDateTime with7Decimals = dateTime.withNano(nanos / 100 * 100);
System.out.println("toString(): " + with7Decimals);
System.out.println("Formatted: " + with7Decimals.format(ODT_FORMATTER));
OffsetDateTime with5Decimals = dateTime.withNano(nanos / 10000 * 10000);
System.out.println("toString(): " + with5Decimals);
System.out.println("Formatted: " + with5Decimals.format(ODT_FORMATTER));
输出:
toString(): 2021-12-10T10:58:05.309594500+01:00 Formatted: 2021-12-10T10:58.05.3095945+01:00 toString(): 2021-12-10T10:58:05.309590+01:00 Formatted: 2021-12-10T10:58.05.30959+01:00
为您测试 我认为没有理由使用字符串进行比较。在将这些小数设置为零之后,只需比较 OffsetDateTime
对象,这些小数无论如何都会在您的数据库中丢失。
编辑: 测试失败的解决方法?下一行是您获取当前时间的地方,其精度比数据库可以存储的精度更高。
dateTime = OffsetDateTime.now(),
因此将其更改为仅提供您的数据库支持的精度:
dateTime = OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS),
这应该会导致保存和检索时间而无需进一步舍入。
为什么会出现这个问题? OffsetDateTime.now()
在不同的平台和Java版本上精度不同。我不知道 Java 11 和 13 在任何平台上有什么区别,但没有理由不应该有一个,而且在某个即将发布的版本中可能会有另一个。此外,如果您在 Linux、Windows 和 Mac 之间移动,即使使用相同的 Java 版本,您也可能会遇到差异。问题的另一部分是数据库的精度有限,或者更准确地说,是数据库中用于存储时间的数据类型。如果您使用的是 timestamp with time zone
,则您可能拥有数据库可以提供的最佳精度。显然,您的数据库具有微秒精度(秒数为 6 位小数),而您的 Java 13 OffsetDateTime.now()
具有至少半微秒(500 纳秒)的精度。所以你从 Java 13 开始得到一个数据库无法存储的值。显然,您的数据库或数据库驱动程序随后会根据您的情况将值四舍五入。这导致从数据库中检索回的值与您尝试保存的值不完全相同。