将字符串转换为日期在不同的设备上会产生不同的结果

Converting String to Date generates different results on different devices

我正在检索一个名为 date 的字符串,格式为 2018-09-20T17:00:00Z,然后使用

将其转换为格式为 Thu Oct 20 17:00:00 GMT+01:00 2018 的日期
SimpleDateFormat dateConvert = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);

convertedDate = new Date();
try {
    convertedDate = dateConvert.parse(date);
} catch (ParseException e) {
    e.printStackTrace();
}

但是在不同的设备上我得到了不同的结果。一个给出 Thu Oct 20 17:00:00 BST 2018(英国夏令时,相当于 GMT+01:00),但这后来被证明是有问题的。有没有办法确保根据 GMT 偏移量格式化日期,即 GMT+01:00 而不是 BST?

您只是在执行两步过程的第一步:

  1. 解析日期以将其转换为 Date 对象
  2. 使用这个已解析的 Date 对象并再次使用 SimpleDateFormat format 它。

那么,您已经正确完成了第一步,下面是您必须对步骤 2 执行的操作,试试这个:

final String formattedDateString = new SimpleDateFormat("EEE MMM dd HH:mm:ss 'GMT'XXX yyyy").format(convertedDate);

Source

java.time

    Instant convertInstant = Instant.parse(date);

一个Instant(就像一个Date)代表一个独立于时区的时间点。所以你很好。作为额外的好处,您的 2018-09-20T17:00:00Z 字符串瞬间采用 ISO 8601 格式,因此 Instant class 无需指定格式即可对其进行解析。

编辑:要将其格式化为具有明确 UTC 偏移量的英国夏令时的人类可读字符串,例如:

    DateTimeFormatter formatter 
            = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss Z yyyy", Locale.UK);
    ZonedDateTime dateTime = convertInstant.atZone(ZoneId.of("Europe/London"));
    String formatted = dateTime.format(formatter);
    System.out.println(formatted);

此代码片段已打印:

Thu Sep 20 18:00:00 +0100 2018

18:00 是偏移量 +01:00 处的正确时间。原始字符串末尾的 Z 表示偏移量零,又名“祖鲁时区”,偏移量零处的 17 与偏移量 +01:00 处的 18:00 是同一时间点。我从你自己的答案中接管了格式模式字符串。

编辑 2

我想向您介绍我根据您自己的回答重写 Fixture class 的建议:

public class Fixture implements Comparable<Fixture> {

    private static DateTimeFormatter formatter 
            = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss Z yyyy", Locale.UK);

    public Instant date;

    /** @param date Date string from either web service or persistence */
    public Fixture(String date) {
        this.date = Instant.parse(date);
    }

    /** @return a string for persistence, e.g., Firebase */
    public String getDateForPersistence() {
        return date.toString();
    }

    /** @return a string for the user in the default time zone of the device */
    public String getFormattedDate() {
        return date.atZone(ZoneId.systemDefault()).format(formatter);
    }

    @Override
    public int compareTo(Fixture other) {
        return date.compareTo(other.date);
    }

    @Override
    public String toString() {
        return "Fixture [date=" + date + "]";
    }

}

此 class 具有自然排序(即按日期和时间),因为它实现了 Comparable,这意味着您不再需要 DateSorter class。几行代码演示了新 getXx 方法的使用:

    String date = "2018-09-24T11:30:00Z";
    Fixture fixture = new Fixture(date);
    System.out.println("Date for user:     " + fixture.getFormattedDate());
    System.out.println("Date for Firebase: " + fixture.getDateForPersistence());

当我在 Europe/London 时区 运行 这段代码时,我得到:

Date for user:     Mon Sep 24 12:30:00 +0100 2018
Date for Firebase: 2018-09-24T11:30:00Z

所以用户得到日期和时间以及他或她自己与 UTC 的偏移量,正如我认为您要求的那样。在 Europe/Berlin 时区尝试相同的片段:

Date for user:     Mon Sep 24 13:30:00 +0200 2018
Date for Firebase: 2018-09-24T11:30:00Z

我们看到德国的用户被告知比赛在 13:30 而不是 12:30,这与他或她的时钟一致。 Firebase中要持久化的日期不变,这也是你想要的。

你的代码出了什么问题

您的格式模式字符串中有两个错误,yyyy-MM-dd'T'hh:mm:ss'Z':

  • 小写字母 hh 表示上午或下午从 01 到 12 的小时,并且仅在带有 AM/PM 标记时才有意义。在实践中你会得到正确的结果除非解析一个小时的 12,这将被理解为 00.
  • 通过将 Z 解析为文字,您无法从字符串中获取 UTC 偏移量信息。相反 SimpleDateFormat 将使用 JVM 的时区设置。这显然因一台设备而异,并解释了为什么您在不同设备上得到不同且相互冲突的结果。

代码中发生的另一件事是 Date.toString 的特殊行为:此方法获取 JVM 的时区设置并使用它来生成字符串。因此,当一个设备设置为 Europe/London 而另一个设备设置为 GMT+01:00 时,相同的 Date 对象将在这些设备上以不同方式呈现。这种行为让很多人感到困惑。

问题:我可以在 Android 上使用 java.time 吗?

是的,java.time 在新旧 Android 设备上都能很好地工作。只需要至少 Java 6.

  • 在 Java 8 和更高版本以及较新的 Android 设备上(据我所知,来自 API 级别 26)现代 API 是内置的。
  • 在 Java 6 和 7 中获取 ThreeTen Backport,新 classes 的 backport(ThreeTen 用于 JSR 310;请参阅底部的链接)。上面的代码是 运行 开发的 org.threeten.bp.Duration 来自 backport。
  • 在(较旧的)Android 使用 ThreeTen Backport 的 Android 版本。它叫做 ThreeTenABP。并确保使用子包从 org.threeten.bp 导入日期和时间 classes。

链接

所以只是想在这里添加一点更新。人们提供的答案帮了大忙,我现在有了一个完全符合我要求的代码,但其中一条评论中的术语 'legacy' 让我觉得可能有更好、更持久的方法。这是代码中当前发生的事情。

1) 我获取了一个足球赛程,它带有 2018-09-22T11:30:00Z

形式的字符串 utc 日期

2) 然后我使用 SimpleDateFormat convertUtcDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);convertUtcDate.setTimeZone(TimeZone.getTimeZone("GMT"));

解析日期

3) 然后我使用 currentTime = Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTime(); 获取当前时间并使用 if(convertedDate.after(currentTime)) 比较两者以找到球队的下一场比赛。在这一点上,我发现一个设备将以相同的形式拥有这两个日期,无论是 BST 还是 GMT+01:00,但无论哪种方式,都可以准确地比较这两个日期。

4) 然后我将日期格式化为使用 SimpleDateFormat convertToGmt = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);String dateString = convertToGmt.format(convertedDate);

的 GMT 偏移量

5) 对于 1) 中的 utc 日期,此 returns Sat Sep 22 12:30:00 GMT+01:00 2018 与设备无关。请注意,时间与 utc 日期不同。不太清楚为什么会这样(可能是因为 运行 API 这个人在德国,比我在英国早一个小时)但重要的是这次是正确的(它指的是明天富勒姆 - 沃特福德的比赛,确实是 12:30 BST/GMT+01:00)。

6) 然后,我将此字符串连同有关灯具的一些其他信息发送到 Firebase。重要的是此时日期的格式为 GMT+01:00 而不是 BST,因为其他设备在读取该信息时可能无法识别 BST 格式。

7) 当谈到从 Firebase 调回该信息时,我通过使用 SimpleDateFormat String2Date = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy", Locale.US); 对其进行解析将其转换回日期,然后我可以通过比较它们的日期按时间顺序排列固定装置。

我想重申一下这个方法是有效的。当英格兰的时区变回 GMT+00:00 时,我检查了固定装置,它仍然工作正常。我试图确保一切都按照格林威治标准时间完成,以便它可以在任何地方工作。我不能确定情况是否如此。有人看到这种方法有什么缺陷吗?可以改进吗?

编辑:这是一段代码,我希望它能简单准确地代表我正在做的事情。

public class FragmentFixture extends Fragment {

SimpleDateFormat convertUtcDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
SimpleDateFormat String2Date = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy", Locale.US);
SimpleDateFormat convertToGmt = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
private List<Fixture> fixtureList;
Date date1;
Date date2;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    convertUtcDate.setTimeZone(TimeZone.getTimeZone("GMT"));
    fixtureList = new ArrayList<>();

    // retrieve fixtures from API and get date for a certain fixture. I will provide an example

    String date = "2018-09-22T11:30:00Z";

    Date convertedDate = new Date();
    try {
        convertedDate = convertUtcDate.parse(date);
    } catch (ParseException e) {
        e.printStackTrace();
    }

    Date currentTime = Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTime();

    if (convertedDate.after(currentTime)) {
        String dateString = convertToGmt.format(convertedDate);
        Fixture fixture = new Fixture(dateString);
        fixtureList.add(fixture);
        Collections.sort(fixtureList, new DateSorter());
    }
}

public class DateSorter implements Comparator<Fixture> {

    @Override
    public int compare(Fixture fixture, Fixture t1) {
        try {
            date1 = String2Date.parse(fixture.getDate());
        } catch (ParseException e) {
            e.printStackTrace();
        }
        try {
            date2 = String2Date.parse(t1.getDate());
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date1.compareTo(date2);
    }
}

public class Fixture {

    public String date;

    public Fixture() {

    }

    public Fixture(String date) {
        this.date = date;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

}

}