英文:
Date is not properly calculated in Java. Issue with zone
问题
以下是翻译好的内容:
我在数据库中有一个日期格式,例如:
Thu Aug 27 2020 00:00:00 GMT-0400 (Eastern Daylight Time)
我想要将相同的日期显示为输出。似乎我漏掉了某个区域信息。输出的日期正在向前演变,变成了这个日期的前一天。
我进行了以下操作:
DateTimeFormatter etFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mma 'ET'");
ZoneId zoneId = ZoneId.of("America/New_York");
ZonedDateTime zonedDateTime = ((Timestamp) date).toLocalDateTime().atZone(zoneId);
etFormat.format(zonedDateTime)
输出:
08/26/2020 at 08:00PM ET
我做错了什么?
英文:
I have a date format stored in DB, for example:
Thu Aug 27 2020 00:00:00 GMT-0400 (Eastern Daylight Time)
I want to display the same date as output. Seems like I am missing something zone. It's evolving to be one day prior to this date.
I did the following:
DateTimeFormatter etFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mma 'ET'");
ZoneId zoneId = ZoneId.of("America/New_York");
ZonedDateTime zonedDateTime = ((Timestamp) date).toLocalDateTime().atZone(zoneId);
etFormat.format(zonedDateTime)
Output:
08/26/2020 at 08:00PM ET
What am I doing wrong?
答案1
得分: 3
在您的数据库中,日期时间带有UTC-04:40的偏移量(假设为美国/纽约时区,比UTC提前4小时40分钟)。当它转换为Timestamp
后,将以没有偏移量的UTC时间存储,即08/26/2020 at 08:00PM
。
因此,首先将Timestamp
转换为UTC的Instant
,然后将Instant
转换为带有时区信息的ZonedDateTime
。
ZonedDateTime dateTime = timestamp.toInstant()
.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of("America/New_York"));
etFormat.format(dateTime); //08/27/2020 at 00:00PM ET
英文:
In your database you have the date time with offset UTC-04:40 (which is 4 hr behind from UTC assuming America/New_York timezone). And when it converts into Timestamp
it will be stores in UTC without offset which is 08/26/2020 at 08:00PM
.
So first convert the Timestamp
into Instant
of UTC and then convert the Instant
into ZonedDateTime
with the zone information
ZonedDateTime dateTime = timestamp.toInstant()
.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of("America/New_York"));
etFormat.format(dateTime); //08/27/2020 at 00:00PM ET
答案2
得分: 1
以下是翻译好的内容:
问题的核心是:
java.sql.Timestamp
,例如resultSet.getTimestamp()
返回的内容,不包含任何时区数据。它只是一个时间点,以自纪元(1970年1月1日)以来的毫秒数存储,使用的是协调世界时(UTC)时区。
这与大多数数据库存储的方式不一致,因为大多数数据库实际上会明确地存储带有时区信息的时间。如果您的数据库不这样做,或者您选择了不包含时区信息的列类型,强烈建议您考虑进行更改。
因此,如果数据库存储了'8月27日纽约午夜',并且数据库被JDBC强制将其转换为java.sql.Timestamp的术语,数据库引擎除了尽力以外无能为力,最好的办法就是返回准确的UTC时间。如果您随后以人类可读的方式打印UTC时间戳,您将得到'凌晨4点',而不是'午夜'(因为纽约比UTC早4小时)。
然后,您的代码会这样说:好的,将时间戳转换为本地日期时间(即'8月27日凌晨4点'的概念,不包含任何时区信息,本身无法再转换回带有更多信息的纪元),然后将其放在纽约时区,从而得到'纽约凌晨4点',比起开始时晚了4个小时。
好的,但是我该如何修复呢?
迄今为止,其他答案都只是提供了一些无关紧要的方法来对抗症状。
我建议您治疗根本问题。
实际错误发生在您要求数据库将其表中完整的时区信息转移到无时区的java.sql.Timestamp对象时。不要这样做。
不要调用(假设您的列名为'mark',请填入实际列名):
resultSet.getTimestamp("mark")
。
调用:
resultSet.getObject("mark", ZonedDateTime.class);
或者可能尝试 LocalDateTime.class
,或者可能是 OffsetDateTime.class
,但推荐使用ZDT。
如果这不起作用,向您的数据库和/或JDBC驱动程序提出投诉,因为它们搞砸了,在与Java端交互时,几乎不可能正确处理时区问题。
实际上,数据库应该只存储某个瞬间
如果存储的时间确实代表了一个'时间瞬间'的概念,而不是'人类可能与您讨论的内容',那么也有相应的数据类型,但请尽快将您的java.sql.Timestamp
对象转换为java.time.Instant
(通过.toInstant()
),或直接要求它:resultSet.getObject("colName", Instant.class)
,让Java和数据库的数据类型立即对齐。
嗯,随便了。治疗方法只适合懦夫,还是绕过它吧
嗯,好吧,您实际上唯一需要做的事情就是不要魔法般地添加4小时。以下代码可以实现:
ZonedDateTime dateTime = timestamp.toInstant()
.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of("America/New_York"));
即使数据库中存储的时区与此不同(这将使您得到纽约时区的那个时间瞬间,例如,如果数据库中存储了'阿姆斯特丹午夜',这将使您得到一个早6小时的时间,或者可能是7小时或5小时,在一年中有几天,由于美国和欧洲在夏令时的变化日不同,情况会有所不同)。
英文:
The central issue is this:
java.sql.Timestamp
, which is what e.g. resultSet.getTimestamp()
returns, does not contain any timezone data. It is simply an instant in time, and it is stored as milliseconds since the epoch (jan 1st, 1970), UTC zone.
This does not match what most DBs store, because most DBs do in fact explicitly store the timezone with that. If your DB does not do this, or you picked a column type which does not do this, you should strongly consider changing that.
So, if the database has stored 'midnight in new york, aug 27th', and the database is forced by JDBC to put this in java.sql.Timestamp terms, there's nothing the DB engine can do about it, other than do its best, which is to return that exact time, in UTC terms. If you then print the UTC timestamp in human terms, you end up with '4 at night', and not 'midnight' (because new york is 4 hours earlier than UTC).
You then, with your code say: Okay, take the timestamp, turn it into a local date time (that'd be the notion of '27th of august, 4 o clock at night', without any inkling of in which czone that is in, and by itself not a thing that can ever be turned back into an epoch with more info), and then you put this at the new york zone, giving you '4 at night in new york', which is 4 hours later than where we started.
Okay, but how do I fix this?
Every other answer (so far) is just giving you silly ways to fight the symptoms.
I propose you fix the disease.
The actual error occurs when you ask the DB to transfer the fully timezoned information from its tables into the timezoneless java.sql.Timestamp object. Stop doing that.
Don't call (I assume your column is called 'mark', fill in whatever it might be):
resultSet.getTimestamp("mark")
.
Call:
resultSet.getObject("mark", ZonedDateTime.class);
or possibly try LocalDateTime.class
, or possibly OffsetDateTime.class
, but ZDT is preferred.
Then if that does not work, complain to your DB and/or JDBC driver because they're messing up and making it next to impossible to do timezone stuff properly when interacting with that DB from the java side.
Actually, the DB should store just a moment-in-time
If truly the time being stored represents the notion of an 'instant in time' and not so much 'as humans would ever talk to you about it', then there are data types for that too, but convert your java.sql.Timestamp
object to a java.time.Instant
asap (via .toInstant()
), or straight up ask for it: resultSet.getObject("colName", Instant.class)
and have java and the db line up the datatypes straight away.
Eh, whatever. Cures are for wussies, just work around it
Eh, well, the only thing you really need to do then is not to magically add 4 hours. This will do it:
ZonedDateTime dateTime = timestamp.toInstant()
.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of("America/New_York"));
even if the tz stored in the DB is something else (it'll then give you that instant in time, but in new york, e.g. if the db has stored 'midnight in amsterdam', this will give you a time 6 hours earlier (or possibly 7 or 5, there are a few days in the year where things go ape due to US and europe having different shift days for daylight savings).
答案3
得分: 0
以下是翻译好的内容:
给出的日期时间格式不正确。通过将您的模式与我的模式进行比较,我希望您能够理解差异。我提供解析逻辑的原因是,您并没有明确指出日期时间的类型。无论是哪种类型,看起来您有一个日期时间字符串:Thu Aug 27 2020 00:00:00 GMT-0400 (Eastern Daylight Time)
,您希望将其解析为ZonedDateTime
并将其显示为您拥有的日期时间字符串的模式。我猜想,您主要遇到的问题是如何将ZonedDateTime
实例格式化为相同的形式。
请按以下方式操作:
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.TextStyle;
public class Main {
public static void main(String[] args) {
// 给定的日期时间字符串
String dateStr = "Thu Aug 27 2020 00:00:00 GMT-0400 (Eastern Daylight Time)";
// 定义用于解析的格式化程序
DateTimeFormatter parsingFormat = new DateTimeFormatterBuilder()
.appendPattern("EEE MMM dd uuuu HH:mm:ss zX")
.appendLiteral(" (")
.appendGenericZoneText(TextStyle.FULL)
.appendLiteral(")")
.toFormatter();
// 将给定的日期时间解析为 ZonedDateTime
ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateStr, parsingFormat);
// 以默认格式显示 [即 zonedDateTime.toString()]
System.out.println(zonedDateTime);
// 定义输出的格式化程序
DateTimeFormatter outputFormat = new DateTimeFormatterBuilder()
.appendPattern("EEE MMM dd uuuu HH:mm:ss z")
.appendLiteral(" (")
.appendPattern("zzzz")
.appendLiteral(")")
.toFormatter();
// 以自定义格式获取字符串表示形式
String strDate = zonedDateTime.format(outputFormat);
// 在自定义格式中显示字符串表示形式
System.out.println(strDate);
}
}
输出:
2020-08-27T00:00-04:00[America/New_York]
Thu Aug 27 2020 00:00:00 GMT-04:00 (Eastern Daylight Time)
注意: 如果您也难以将时间戳转换为ZonedDateTime
,您可以参考本页面上的其他答案,并使用此答案来解决格式化问题。
英文:
The format that you have used is not correct. I hope you will be able to understand the difference by comparing your pattern with mine. The reason why I've presented the parsing logic is that you have not made it clear the type of date-time. Whatever type it may be, it looks like you have a date-time string, Thu Aug 27 2020 00:00:00 GMT-0400 (Eastern Daylight Time)
which you want to parse into ZonedDateTime
and display the same into the pattern of the date-time string you have. I guess, the main problem you are having is how to format the ZonedDateTime
instance into the same form.
Do it as follows:
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.TextStyle;
public class Main {
public static void main(String[] args) {
// Given date-time string
String dateStr = "Thu Aug 27 2020 00:00:00 GMT-0400 (Eastern Daylight Time)";
// Define the formatter for parsing
DateTimeFormatter parsingFormat = new DateTimeFormatterBuilder()
.appendPattern("EEE MMM dd uuuu HH:mm:ss zX")
.appendLiteral(" (")
.appendGenericZoneText(TextStyle.FULL)
.appendLiteral(")")
.toFormatter();
// Parse the given date-time into ZonedDateTime
ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateStr, parsingFormat);
// Display in default format [i.e. zonedDateTime.toString()]
System.out.println(zonedDateTime);
// Define the formatter for output
DateTimeFormatter outputFormat = new DateTimeFormatterBuilder()
.appendPattern("EEE MMM dd uuuu HH:mm:ss z")
.appendLiteral(" (")
.appendPattern("zzzz")
.appendLiteral(")")
.toFormatter();
// Get the string representation in the custom format
String strDate = zonedDateTime.format(outputFormat);
// Display the string representation in the custom format
System.out.println(strDate);
}
}
Output:
2020-08-27T00:00-04:00[America/New_York]
Thu Aug 27 2020 00:00:00 GMT-04:00 (Eastern Daylight Time)
Note: By any chance, if you also have difficulty to convert the timestamp into ZonedDateTime
, you can refer other answers on this page and use this answer to solve the problem with formatting.
答案4
得分: 0
日期转换使用时区设置为GMT。
final static String datePattern = "EEE MM/dd/yyyy HH:mm:ss 'GMT'Z '('z')'";
DateFormat df = new SimpleDateFormat(datePattern, Locale.getDefault());
simpledateformat.setTimeZone(TimeZone.getTimeZone("GMT"));
simpleDateFormat.format(givenDate);
英文:
The date is converted with timezone set to GMT.
final static String datePattern = "EEE MM/dd/yyyy HH:mm:ss 'GMT'Z '('z')'";
DateFormat df = new SimpleDateFormat(datePattern, Locale.getDefault());
simpledateformat.setTimeZone(TimeZone.getTimeZone("GMT"))
simpleDateFormat.format(givenDate)
答案5
得分: 0
java.time
我建议您在处理日期时,完全使用现代的 Java 日期和时间 API,即 java.time
。不要再从数据库获取 Date
或 Timestamp
,因为从 JDBC 4.2 开始(对于 MySQL 来说,这已经有很多年了),请从结果集中获取现代的 LocalDate
。以下是一个示例:
PreparedStatement ps = yourDatabaseConnection.prepareStatement("select your_date from your_table;");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
LocalDate date = rs.getObject("your_date", LocalDate.class);
// 对日期进行处理
}
LocalDate
是一个不包含时间和时区信息的日期对象。因此,这样做可以避免所有与时区相关的问题。
如果您想以北美东部时区的时间为基准,并按照问题中提到的格式将一天的开始时间显示给用户:
DateTimeFormatter etFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mma v");
ZoneId zoneId = ZoneId.of("America/New_York");
LocalDate date = LocalDate.of(2020, Month.AUGUST, 27);
ZonedDateTime startOfDay = date.atStartOfDay(zoneId);
String result = startOfDay.format(etFormat);
System.out.println(result);
这个示例的输出将会是:
> 08/27/2020 at 12:00AM ET
在格式模式中使用模式字母 v
来表示时区,而不是硬编码为 ET
。后者会在将来有一天,一位初级程序员将一个在其他时区的 ZonedDateTime
传递给代码时,产生错误和混淆的结果。
代码中出了什么问题?
我不太清楚您是如何从数据库中获取 date
的。显然,尽管声明为 Date
,但 date
实际上是一个 Timestamp
(这是一种不好的做法,因为这两个类之间的继承关系实际上是一种实现关系,而不是概念上的关系),它表示的是 UTC 时区的一天的开始。toLocalDateTime()
是一个危险且常常没有意义的调用:它使用 JVM 的时区将 Timestamp
转换为 LocalDateTime
。在 UTC 0:00 时,东部时区的时间是前一天晚上的 8 点,因此您的 LocalDateTime
将变为 2020-08-26T20:00。接下来,atZone(zoneId)
之所以给出正确的时间,是因为 zoneId
恰好与前一步中 JVM 使用的时区相符。
链接
- Oracle 教程:日期时间 解释了如何使用
java.time
。
英文:
java.time
I recommend that you use java.time, the modern Java date and time API, exclusively for your date work. Instead of getting a Date
or Timestamp
from your database, since JDBC 4.2 (in the case of MySQL that’s many years now) get a modern LocalDate
from your result set. An example:
PreparedStatement ps = yourDatabaseConnection.prepareStatement("select your_date from your_table;");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
LocalDate date = rs.getObject("your_date", LocalDate.class);
// Do something with date
}
A LocalDate
is a date without time of day and without time zone. So this will relieve you of all time zone trouble.
If you want to print the start of the day in North American Eastern time zone to the user in the format used in the question:
DateTimeFormatter etFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mma v");
ZoneId zoneId = ZoneId.of("America/New_York");
LocalDate date = LocalDate.of(2020, Month.AUGUST, 27);
ZonedDateTime startOfDay = date.atStartOfDay(zoneId);
String result = startOfDay.format(etFormat);
System.out.println(result);
Output from this example is:
> 08/27/2020 at 12:00AM ET
Do use pattern letter v
for time zone in the format pattern rather than hard-coding ET
. The latter will produce false and confusing results when one day a junior programmer feeds a ZonedDateTime
in an other time zone into the code.
What went wrong in your code?
It’s not clear to me how you got your date
from your database. Apparently date
even though declared a Date
was really a Timestamp
(a bad practice since the inheritance relationship between the two classes is really one of implementation, not a conceptual one) denoting the start of the day in UTC. toLocalDateTime()
is a dangerous and often meaningless call: it uses the time zone of the JVM for converting the Timestamp
to a LocalDateTime
. At 0:00 UTC it is 8 PM the evening before in Eastern time zone, so your LocalDateTime
becomes 2020-08-26T20:00. Next atZone(zoneId)
only gives the correct time because zoneId
happens to coincide with the JVM’s time zone used in the previous step.
Link
- Oracle tutorial: Date Time explaining how to use java.time.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论