为什么与UTC相同偏移的时区显示不同的时间?

时间:2015-05-23 06:42:53

标签: java date datetime

我今天碰到了这个问题。我已将时钟设置为UTC-6.00(中美洲)时区。我正在转换日期" 06/01/2015 :: 12:00:00 AM" (" MM / dd / yyyy :: hh:mm:ss a"格式化)到Java Date对象。然后我将date对象重新转换为String。虽然我这样做有一点点扭曲。我列出了以下重新转换步骤 -

  1. 计算当前时区的UTC偏移量。 (-21600000)
  2. 获取此偏移量的所有可用时区ID。 (都有相同的偏移量)
  3. 选择第一个时区ID。 (将有相同的偏移量)
  4. 将此设置为时区。
  5. 使用Java的简单日期格式将日期转换为字符串格式。
  6. 我看到现在呈现的时间是" 06/01/2015 :: 01:00:00 AM"

    我的问题:

    1. 由于在创建过程中和转换过程中时区偏移量相同,我希望显示相同的时间。但我所看到的是不同的。为什么会这样?

    2. 想象一下,在服务器中进行重新转换以及在客户端中进行创建。我需要向客户端渲染相同的日期和时间。我该怎么做?

    3. 请帮忙!非常感谢任何帮助。

      编辑:以下是代码。请注意,我已将当前时区设置为中美洲。

      public class TimeTest {
      
      public static void main (String args[]) {
      
          SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy::hh:mm:ss a");
          String dateInString = "01/06/2015::12:00:00 AM";
      
          try {    
              Date date = formatter.parse(dateInString);
              System.out.println("Before conversion --> " + formatter.format(date));
              System.out.println("After conversion --> " + convertDateValueIntoString(date));
      
      
          } catch (ParseException e) {
              e.printStackTrace();
          }       
      }
      
      private static String convertDateValueIntoString(Date dateValue){
          SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy::hh:mm:ss a");       
          String date;
          int offset = TimeZone.getDefault().getRawOffset();
          if (offset == 0) {
              dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
              date = dateFormat.format(dateValue);
          } else {        
              String TZ[] = TimeZone.getAvailableIDs(offset);
              String timeZone = TZ[0];
              if (timeZone == null) {
                  date = dateFormat.format(dateValue);
              } else {
                  TimeZone tz = TimeZone.getTimeZone(timeZone);
                  dateFormat.setTimeZone(tz);
                  date = dateFormat.format(dateValue);
              }           
          }
      
          return date;
      }
      }
      

3 个答案:

答案 0 :(得分:4)

  1. 为什么时代不同:
  2. 差异似乎在于夏令时的处理。将我的机器设置到不同的时区并打印TimeZone toString(),我最终得到了:

    Initial: sun.util.calendar.ZoneInfo[id="America/Tegucigalpa",offset=-21600000,dstSavings=0,useDaylight=false,transitions=9,lastRule=null]
    Result: sun.util.calendar.ZoneInfo[id="America/Bahia_Banderas",offset=-21600000,dstSavings=3600000,useDaylight=true,...
    

    请注意,这两个时区具有相同的偏移量,但其中一个使用夏令时,另一个则不使用。偏移量是您的所有代码都在寻找合适的TimeZone,但日期格式也使用夏令时偏移量。

    1. 我该如何处理:
    2. 我曾经使用过的每个项目的方式都是将所有内部时间表示为UTC(或类似的概念)。我希望您的客户端在输入时将时间转换为UTC(在将其发送到服务器之前),让所有服务器存储使用UTC,然后当返回客户端时将客户端格式设置为默认TimeZone仅用于输出给用户

      这样,您的所有内部时间都是一致的,并且所有显示的时间都是针对客户端的单个实例进行本地化的,因此America / Tegucigalpa中的用户可能会将时间设置为12:00,但是America / Bahia_Banderas中的用户会看到1:00。对于那些将要显示的用户来说,两者都是正确的。

答案 1 :(得分:3)

Answer by 1337joe是正确的。我会添加一些想法。

这个问题有很多混乱。

时区=偏移+规则/异常/调整

首先,时区大于UTC的偏移量。时区是偏移量加上一组关于夏令时和其他异常的过去,现在和将来的规则。调整。

因此,只要有可能,请使用named time zone而不是仅仅偏移。当然,不要将偏移量的使用与时区的使用混合使用,并期望得到合理的结果。这似乎是本课题的核心问题。

因此,深入挖掘发现设计现有存储数据的程序员的初衷。我怀疑他们确实有一个特定的时区而不仅仅是一个偏移。

使用适当的时区名称

没有“中美洲”这样的时区。

正如1337Joe所指出的那样,中美洲的偏移量和时区各不相同。例如,America/Managua比UTC低6小时,而America/Panama为5。

顺便说一句,避免使用“EST”等时区的3-4个字母代码,因为它们既不标准也不唯一。当然,唯一的例外是UTC

指定预期/期望的时区

当[a]你知道你的输入数据代表一个特定的时区或偏移时,虽然是隐含的,并且[b]你希望应用某个时区,但是不要在默认的时区上调用。那是在惹麻烦。默认时区可能因机器上的主机操作系统设置而异。并且管理员可以随时更改主机操作系统设置。第三,通过在同一JVM中的任何应用程序中的任何线程中调用TimeZone.setDefault(),可以随时在运行时中更改JVM的当前默认时区。

因此,不要依赖默认时区,而是指定所需的时区。

使用UTC For Logic&存储

正如1337joe所说,您的业务逻辑,数据存储,数据通信和数据库都应该是UTC(几乎总是)。仅在用户/消费者预期时对本地时区应用调整。

在评论中,作者说他们的项目已经背负着隐含代表某个时区或偏移的现有存储数据。

java.util.Date toString

java.util.Date上的toString方法自动应用JVM的当前默认时区。这使得处理时区调整变得棘手。避免使用java.util.Date/.Calendar&的许多原因之一java.text.SimpleDateFormat类。

使用更好的日期时间库

使用Java 8及更高版本(java.time package)中的新TutorialJoda-Time库(启发java.time)。

约达时间

以下是Joda-Time中的一些示例代码。

根据作者的评论,传入的字符串隐含地表示某个已知时区的日期时间值。该时区没有说明,所以我将随意使用巴拿马时区。在第一部分中,我们解析一个字符串,同时指定在解析期间使用的时区并分配给结果对象。

DateTimeZone zonePanama = DateTimeZone.forID( "America/Panama" );
DateTimeFormatter formatter = DateTimeFormat.forPattern( "dd/MM/yyyy::hh:mm:ss a" );
String input = "06/01/2015::12:00:00 AM";
DateTime dateTimePanama = formatter.withZone( zonePanama ).parseDateTime( input );
System.out.println( "Input as string: " + input + " becomes object: " + dateTimePanama + " with time zone: " + dateTimePanama.getZone() );

现在让我们调整为UTC。这是为了演示。在实际代码中,您通常会使用此UTC值进行任何进一步的工作。

DateTime dateTimeUtc = dateTimePanama.withZone( DateTimeZone.UTC );
System.out.println( "dateTimeUtc: " + dateTimeUtc );

对于输出,我们的用户/消费者期望在相同的巴拿马时区中使用字符串表示,格式与我们的输入相同。

String output = formatter.print( dateTimeUtc.withZone( zonePanama ) );
System.out.println( "Output in special format: " + output );

跑步时。

Input as string: 06/01/2015::12:00:00 AM becomes object: 2015-01-06T00:00:00.000-05:00 with time zone: America/Panama
dateTimeUtc: 2015-01-06T05:00:00.000Z
Output in special format: 06/01/2015::12:00:00 AM

答案 2 :(得分:2)

对于问题#1:不同时区的时区偏移量可能相同,但可以使用或不使用DST,这会产生差异。

对于问题#2:

将来,您只能对使用UTC的时间安全。 (你可以解决,如果你的时间数据是"最近" - 见下文)

过去,您无法可靠地提取正确的时间。

一般转化建议:

我在JDBC驱动程序中处理了一个处理时区和DST的项目。存储时间值并正确读取它们存在问题。我努力/真正努力/尝试获得正确的转换,因此我们可以将更大的工作转换为UTC。没有UTC,没有正确的转换。 (/真的很难/:想想纸浆小说朱尔斯说的那些"我真的很难成为牧羊人。" :-))

问题#2 /未来:

如果您的客户端无法发送UTC时间(可能是因为它是第三方系统):

当您的服务器从客户端收到时间数据(非UTC)时,您知道它在几分钟内(可能稍长一些)是最新的,您可以尝试使用您的UTC时间并将其与客户端相匹配。时间。想象一下,您的客户发送" 2015-06-01 15:45"而且你知道,它现在是" 2015-06-01 18:51 UTC",那么你可以将客户的时间解释为" 2015-06-01 18:45 UTC&# 34 ;.如果客户端发送的时间数据可能超过大约一小时,则在某些情况下会失败。

或换句话说:假设您的客户记录温度值。如果客户端发送的数据不超过几分钟,则可以将其与UTC时间进行匹配。如果您的客户记录了一天的温度并在一天结束时将其发送给您,则无法正确匹配时间。

为什么您无法完全(!)正确转换?

假设DST改变的夜晚,时钟从03:00变回02:00。你有一次02:30开关前和另一个02:30开关后。第一个02:30的另一个UTC时间比第二个02:30。所以使用UTC你很好。但只有"客户本地" 02:30,你永远不会确定。

回到客户端数据时代:如果您的客户端在02:30发送不超过几分钟的数据,然后在第二个02:30发送另一个数据,则可以在服务器上区分这些数据。如果在04:00你得到02:30的两条记录,则无法再恢复UTC。

问题#2 /过去:

您可以在数据库中添加一个标志,以便以UTC标记的新时间被标记为"可靠"旧的价值观不是吗?

输出和来源:

在我的系统上运行修改后的源的输出,其TZ为"欧洲/柏林"。请注意,这已使用DST,但第一个获取的TZ(" Algiers")已使用DST。

formatter's TZ is sun.util.calendar.ZoneInfo[id="Europe/Berlin",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Berlin,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]
internal date value = 1433109600000 as UTC = 31/05/2015::10:00:00 PM
Before conversion --> 01/06/2015::12:00:00 AM
Conversion: offset != 0, using TZ sun.util.calendar.ZoneInfo[id="Africa/Algiers",offset=3600000,dstSavings=0,useDaylight=false,transitions=35,lastRule=null]
After conversion --> 31/05/2015::11:00:00 PM

Setting UTC...

formatter's TZ is sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
internal date value = 1433116800000 as UTC = 01/06/2015::12:00:00 AM
Before conversion --> 01/06/2015::12:00:00 AM
Conversion: offset != 0, using TZ sun.util.calendar.ZoneInfo[id="Africa/Algiers",offset=3600000,dstSavings=0,useDaylight=false,transitions=35,lastRule=null]
After conversion --> 01/06/2015::01:00:00 AM

源代码:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class TimeTest {
    static TimeZone utc = TimeZone.getTimeZone("UTC");

    public static void main (String args[]) {

        SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy::hh:mm:ss a");
        String dateInString = "01/06/2015::12:00:00 AM";
        SimpleDateFormat utcformatter = new SimpleDateFormat("dd/MM/yyyy::hh:mm:ss a");
        utcformatter.setTimeZone(utc);

        try {    
            Date date = formatter.parse(dateInString);
            System.out.println("formatter's TZ is " + formatter.getTimeZone());
            System.out.println("internal date value = " +  date.getTime() + " as UTC = " + utcformatter.format(date));
            System.out.println("Before conversion --> " + formatter.format(date));
            System.out.println("After conversion --> " + convertDateValueIntoString(date));

            System.out.println("\nSetting UTC...\n");
            formatter.setTimeZone(utc);

            date = formatter.parse(dateInString);
            System.out.println("formatter's TZ is " + formatter.getTimeZone());
            System.out.println("internal date value = " +  date.getTime() + " as UTC = " + utcformatter.format(date));
            System.out.println("Before conversion --> " + formatter.format(date));
            System.out.println("After conversion --> " + convertDateValueIntoString(date));
        } catch (ParseException e) {
            e.printStackTrace();
        }       
    }

    private static String convertDateValueIntoString(Date dateValue){
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy::hh:mm:ss a");       
        String date;
        int offset = TimeZone.getDefault().getRawOffset();
        if (offset == 0) {
            System.out.println("Conversion: offset == 0 -- setting UTC");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
            date = dateFormat.format(dateValue);
        } else {        
            String TZ[] = TimeZone.getAvailableIDs(offset);
            String timeZone = TZ[0];
            if (timeZone == null) {
                System.out.println("Conversion: offset != 0, did not find TZ, tz of dateFormat is " + dateFormat.getTimeZone());
                date = dateFormat.format(dateValue);
            } else {
                TimeZone tz = TimeZone.getTimeZone(timeZone);
                System.out.println("Conversion: offset != 0, using TZ " + tz);
                dateFormat.setTimeZone(tz);
                date = dateFormat.format(dateValue);
            }           
        }

        return date;
    }
}