如何使用 Java 处理日历时区?

Posted

技术标签:

【中文标题】如何使用 Java 处理日历时区?【英文标题】:How to handle calendar TimeZones using Java? 【发布时间】:2010-09-18 19:27:30 【问题描述】:

我有一个来自我的应用程序的时间戳值。用户可以在任何给定的本地时区。

由于此日期用于假定给定时间始终为 GMT 的 WebService,因此我需要将用户的参数从 (EST) 转换为 (GMT)。这是踢球者:用户忘记了他的 TZ。他输入他要发送给 WS 的创建日期,所以我需要的是:

用户输入: 2008 年 5 月 1 日下午 6:12(美国东部标准时间)WS 的参数需要是:2008 年 5 月 1 日 6下午 12 点(格林威治标准时间)

我知道默认情况下 TimeStamps 总是应该在 GMT 中,但是在发送参数时,即使我从 TS(应该在 GMT 中)创建了我的日历,除非用户是,否则时间总是关闭的在格林威治标准时间。我错过了什么?

Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
...
private static java.util.Calendar convertTimestampToJavaCalendar(Timestamp ts_) 
  java.util.Calendar cal = java.util.Calendar.getInstance(
      GMT_TIMEZONE, EN_US_LOCALE);
  cal.setTimeInMillis(ts_.getTime());
  return cal;

使用之前的代码,我得到的结果是这样的(短格式便于阅读):

[2008 年 5 月 1 日晚上 11:12]

【问题讨论】:

为什么只更改时区而不转换日期/时间? 获取一个时区的 Java 日期,然后在另一个时区获取 那个 日期是一件非常痛苦的事情。即,在美国东部夏令时间下午 5 点参加,并在太平洋夏令时间下午 5 点参加。 【参考方案1】:

过去对我有用的方法是确定用户时区与 GMT 之间的偏移量(以毫秒为单位)。获得偏移量后,您可以简单地加/减(取决于转换的方式)以获得任一时区的适当时间。我通常会通过设置 Calendar 对象的毫秒字段来完成此操作,但我相信您可以轻松地将其应用于时间戳对象。这是我用来获取偏移量的代码

int offset = TimeZone.getTimeZone(timezoneId).getRawOffset();

timezoneId 是用户所在时区的id(如EST)。

【讨论】:

使用您忽略 DST 的原始偏移量。 没错,这种方法会给出半年的错误结果。最糟糕的错误形式。【参考方案2】:

您的 TimeStamp 似乎被设置为原始系统的时区。

这已被弃用,但它应该可以工作:

cal.setTimeInMillis(ts_.getTime() - ts_.getTimezoneOffset());

不推荐使用的方式是使用

Calendar.get(Calendar.ZONE_OFFSET) + Calendar.get(Calendar.DST_OFFSET)) / (60 * 1000)

但这需要在客户端完成,因为该系统知道它所在的时区。

【讨论】:

【参考方案3】:
public static Calendar convertToGmt(Calendar cal) 

    Date date = cal.getTime();
    TimeZone tz = cal.getTimeZone();

    log.debug("input calendar has date [" + date + "]");

    //Returns the number of milliseconds since January 1, 1970, 00:00:00 GMT 
    long msFromEpochGmt = date.getTime();

    //gives you the current offset in ms from GMT at the current date
    int offsetFromUTC = tz.getOffset(msFromEpochGmt);
    log.debug("offset is " + offsetFromUTC);

    //create a new calendar in GMT timezone, set to this date and add the offset
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.setTime(date);
    gmtCal.add(Calendar.MILLISECOND, offsetFromUTC);

    log.debug("Created GMT cal with date [" + gmtCal.getTime() + "]");

    return gmtCal;

如果我通过当前时间(Calendar.getInstance() 中的“12:09:05 EDT”),以下是输出:

调试 - 输入日历的日期为 [Thu Oct 23 12:09:05 EDT 2008] 调试 - 偏移量为 -14400000 调试 - 使用日期创建 GMT 校准 [Thu Oct 23 08:09:05 EDT 2008]

格林威治标准时间 12:09:05 是美国东部时间 8:09:05。

这里令人困惑的部分是Calendar.getTime() 在您当前的时区中返回一个Date,并且没有任何方法可以修改日历的时区并滚动基础日期。根据您的 Web 服务采用的参数类型,您可能只想让 WS 以从纪元开始的毫秒数为单位进行处理。

【讨论】:

您不应该减去offsetFromUTC 而不是添加它吗?使用您的示例,如果 12:09 GMT 是 8:09 EDT(这是真的),并且用户输入“12:09 EDT”,我认为算法应该输出“16:09 GMT”。【参考方案4】:

感谢大家的回复。经过进一步的调查,我得到了正确的答案。正如 Skip Head 所提到的,我从应用程序中获取的时间戳正在调整为用户的时区。因此,如果用户输入 6:12 PM (EST) 我会得到 2:12 PM (GMT)。我需要的是一种撤消转换的方法,这样用户输入的时间就是我发送到 WebServer 请求的时间。以下是我完成此操作的方法:

// Get TimeZone of user
TimeZone currentTimeZone = sc_.getTimeZone();
Calendar currentDt = new GregorianCalendar(currentTimeZone, EN_US_LOCALE);
// Get the Offset from GMT taking DST into account
int gmtOffset = currentTimeZone.getOffset(
    currentDt.get(Calendar.ERA), 
    currentDt.get(Calendar.YEAR), 
    currentDt.get(Calendar.MONTH), 
    currentDt.get(Calendar.DAY_OF_MONTH), 
    currentDt.get(Calendar.DAY_OF_WEEK), 
    currentDt.get(Calendar.MILLISECOND));
// convert to hours
gmtOffset = gmtOffset / (60*60*1000);
System.out.println("Current User's TimeZone: " + currentTimeZone.getID());
System.out.println("Current Offset from GMT (in hrs):" + gmtOffset);
// Get TS from User Input
Timestamp issuedDate = (Timestamp) getACPValue(inputs_, "issuedDate");
System.out.println("TS from ACP: " + issuedDate);
// Set TS into Calendar
Calendar issueDate = convertTimestampToJavaCalendar(issuedDate);
// Adjust for GMT (note the offset negation)
issueDate.add(Calendar.HOUR_OF_DAY, -gmtOffset);
System.out.println("Calendar Date converted from TS using GMT and US_EN Locale: "
    + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
    .format(issueDate.getTime()));

代码的输出是:(用户输入 5/1/2008 6:12PM (EST)

当前用户的时区:EST 当前与 GMT 的偏移量(以小时为单位):-4(通常为 -5,DST 调整除外) 来自 ACP 的 TS:2008-05-01 14:12:00.0 使用 GMT 和 US_EN 从 TS 转换的日历日期 区域设置:2008 年 5 月 1 日下午 6:12 (GMT)

【讨论】:

【参考方案5】:

你说日期是与网络服务结合使用的,所以我假设它在某个时候被序列化为一个字符串。

如果是这种情况,您应该查看 DateFormat 类的setTimeZone method。这决定了在打印时间戳时将使用哪个时区。

一个简单的例子:

SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));

Calendar cal = Calendar.getInstance();
String timestamp = formatter.format(cal.getTime());

【讨论】:

不依赖 SDF 如果它有自己的时区,想知道为什么更改日历时区似乎没有效果! TimeZone.getTimeZone("UTC") 无效,因为 UTC 不在 AvailableIDs() 中……天知道为什么 TimeZone.getTimeZone("UTC") 在我的机器上可用。它依赖于 JVM 吗? setTimeZone() 方法的好主意。你帮我省了很多麻烦,非常感谢! 正是我需要的!您可以在格式化程序中设置服务器时区,然后当您将其转换为日历格式时,您无需担心。迄今为止最好的方法!对于 getTimeZone,您可能需要使用格式 Ex:ETD 的“GMT-4:00”【参考方案6】:

从一个 timeZone 转换为另一个 timeZone 的方法(可能有效:))。

/**
 * Adapt calendar to client time zone.
 * @param calendar - adapting calendar
 * @param timeZone - client time zone
 * @return adapt calendar to client time zone
 */
public static Calendar convertCalendar(final Calendar calendar, final TimeZone timeZone) 
    Calendar ret = new GregorianCalendar(timeZone);
    ret.setTimeInMillis(calendar.getTimeInMillis() +
            timeZone.getOffset(calendar.getTimeInMillis()) -
            TimeZone.getDefault().getOffset(calendar.getTimeInMillis()));
    ret.getTime();
    return ret;

【讨论】:

【参考方案7】:

你可以用Joda Time解决它:

Date utcDate = new Date(timezoneFrom.convertLocalToUTC(date.getTime(), false));
Date localDate = new Date(timezoneTo.convertUTCToLocal(utcDate.getTime()));

Java 8:

LocalDateTime localDateTime = LocalDateTime.parse("2007-12-03T10:15:30");
ZonedDateTime fromDateTime = localDateTime.atZone(
    ZoneId.of("America/Toronto"));
ZonedDateTime toDateTime = fromDateTime.withZoneSameInstant(
    ZoneId.of("Canada/Newfoundland"));

【讨论】:

注意这一点,但是如果您使用的是 hibernate 4,它在没有侧面依赖和额外配置的情况下是不直接兼容的。然而,对于第 3 版来说,它是最快和最容易使用的方法。【参考方案8】:

DateTimestamp 对象与时区无关:它们表示自纪元以来的特定秒数,而不承诺将该时刻的特定解释为小时和天. 时区仅在 GregorianCalendar(此任务不直接需要)和 SimpleDateFormat 中输入图片,这需要时区偏移量才能在单独的字段和 Date(或 long ) 值。

OP 的问题就在他处理的开始:用户输入的小时数不明确,并且在本地非 GMT 时区解释;此时值为 "6:12 EST",可以轻松打印为 "11.12 GMT" 或任何其他时区,但永远不会更改“格林威治标准时间 6.12”

没有办法将 "06:12" 解析为 "HH:MM"SimpleDateFormat (默认为本地时区)默认为 UTC; SimpleDateFormat 有点太聪明了。

但是,如果您将其明确地放入输入中,您可以说服任何 SimpleDateFormat 实例使用正确的时区:只需将一个固定字符串附加到接收到的(并经过充分验证的)” 06:12""06:12 GMT" 解析为 "HH:MM z"

不需要显式设置 GregorianCalendar 字段或检索和使用时区和夏令时偏移。

真正的问题是将默认为本地时区的输入、默认为 UTC 的输入和真正需要明确时区指示的输入分开。

【讨论】:

【参考方案9】:

java.time

现代方法使用 java.time 类取代了与最早版本的 Java 捆绑在一起的麻烦的遗留日期时间类。

java.sql.Timestamp 类是这些遗留类之一。不再需要。而是使用 JDBC 4.2 及更高版本直接将 Instant 或其他 java.time 类与您的数据库一起使用。

Instant 类表示UTC 中时间轴上的时刻,分辨率为nanoseconds(最多九 (9) 位小数)。

Instant instant = myResultSet.getObject( … , Instant.class ) ; 

如果您必须与现有的Timestamp 互操作,请通过添加到旧类的新转换方法立即转换为 java.time。

Instant instant = myTimestamp.toInstant() ;

要调整到另一个时区,请将时区指定为ZoneId 对象。以continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 3-4 个字母的伪时区,例如 ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的(!)。

ZoneId z = ZoneId.of( "America/Montreal" ) ;

应用到Instant 以生成ZonedDateTime 对象。

ZonedDateTime zdt = instant.atZone( z ) ;

要生成显示给用户的字符串,请在 Stack Overflow 中搜索 DateTimeFormatter 以查找许多讨论和示例。

您的问题实际上是关于从用户数据输入到日期时间对象的另一个方向。通常最好将您的数据输入分成两部分,一个日期和一个时间。

LocalDate ld = LocalDate.parse( dateInput , DateTimeFormatter.ofPattern( "M/d/uuuu" , Locale.US ) ) ;
LocalTime lt = LocalTime.parse( timeInput , DateTimeFormatter.ofPattern( "H:m a" , Locale.US ) ) ;

您的问题不清楚。是否要将用户输入的日期和时间解释为 UTC?还是在其他时区?

如果您指的是 UTC,请使用 UTC 常量 ZoneOffset.UTC 创建一个带有偏移量的 OffsetDateTime

OffsetDateTime odt = OffsetDateTime.of( ld , lt , ZoneOffset.UTC ) ;

如果您指的是另一个时区,请结合时区对象 ZoneId。但是哪个时区?您可能会检测到默认时区。或者,如果关键,您必须与用户确认他们的意图。

ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

要获得一个根据定义始终采用 UTC 的更简单对象,请提取 Instant

Instant instant = odt.toInstant() ;

……或者……

Instant instant = zdt.toInstant() ; 

发送到您的数据库。

myPreparedStatement.setObject( … , instant ) ;

关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。

从哪里获得 java.time 类?

Java SE 8Java SE 9 及更高版本 内置。 标准 Java API 的一部分,带有捆绑实现。 Java 9 添加了一些小功能和修复。 Java SE 6Java SE 7 ThreeTen-Backport 中的大部分 java.time 功能都向后移植到 Java 6 和 7。 Android java.time 类的 android 捆绑包实现的更高版本。 对于早期的 Android,ThreeTenABP 项目采用 ThreeTen-Backport(如上所述)。见How to use ThreeTenABP…

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuarter 和more。

【讨论】:

以上是关于如何使用 Java 处理日历时区?的主要内容,如果未能解决你的问题,请参考以下文章

如何从日历中获取 UTC 时间戳?

如何使用 Selenium WebDriver 和 Java 处理日历弹出窗口?

如何使用 fullcalendar 处理 DST 显示混乱

iPhone 日历和时区 (EventKit)

我的应用程序如何获取用户 iPhone 上的日历列表

在Java中构造没有时区的日历