【问题标题】:Converting String to Date generates different results on different devices将字符串转换为日期会在不同的设备上产生不同的结果
【发布时间】:2018-09-20 16:47:05
【问题描述】:

例如,我正在以 2018-09-20T17:00:00Z 的形式检索一个名为 date 的字符串,并将其转换为 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(英国夏令时间,相当于格林威治标准时间+01:00),但后来证明这是有问题的。有没有办法确保日期格式为 GMT 偏移量,即 GMT+01:00 而不是 BST?

【问题讨论】:

  • 您也可以发布您的格式化代码吗?
  • 好的,我已经更新了帖子
  • 你不打电话给dateConvert.format(...)吗?
  • 我不是。我可以做?我一开始检索的日期是字符串,而不是日期,所以我解析字符串以获得日期
  • 仅供参考,java.util.Datejava.util.Calendarjava.text.SimpleDateFormat 等麻烦的旧日期时间类现在已被 java.time 类所取代。大多数 java.time 功能在 ThreeTen-Backport 项目中被反向移植到 Java 6 和 Java 7。在ThreeTenABP 项目中进一步适用于早期的Android。见How to use ThreeTenABP…

标签: android timezone date-parsing


【解决方案1】:

java.time

    Instant convertInstant = Instant.parse(date);

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

编辑:在英国夏令时将其格式化为人类可读的字符串,并使用明确的 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);

这个代码sn-p打印出来:

2018 年 9 月 20 日星期四 18:00:00 +0100

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

编辑 2

我想向您提出我的建议,即根据您自己的答案重写 Fixture 类:

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 + "]";
    }

}

此类具有自然排序(即按日期和时间),因为它实现了Comparable,这意味着您不再需要您的DateSorter 类。几行代码来演示新的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());

当我在欧洲/伦敦时区运行这个 sn-p 时,我得到:

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

因此,正如我认为您所要求的那样,用户可以使用他或她自己的 UTC 偏移量来获取日期和时间。在欧洲/柏林时区尝试相同的 sn-p:

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 中的小时,并且仅对 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,即新类的后向端口(对于 JSR 310,ThreeTen;请参阅底部的链接)。上面的代码是使用来自 backport 的 org.threeten.bp.Duration 开发和运行的。
  • 在(较旧的)Android 上使用 ThreeTen Backport 的 Android 版本。它被称为 ThreeTenABP。并确保从 org.threeten.bp 导入日期和时间类以及子包。

链接

【讨论】:

  • 感谢您非常彻底的回答。我不太了解hhHH 之间的区别,但事实证明这很有帮助。
  • 请看我的最新回答。我会很感激您对此的想法,因为您似乎非常了解 Java 中的日期和时间,而我却没有,哈哈!
  • 你可能想在你的答案中发布可运行的代码(类似于Minimal, Complete, and Verifiable example),我会在周末后再看一下。
【解决方案2】:

你只是在做一个两步过程的第一步:

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

所以,你已经正确地完成了第一步,下面是你对第 2 步要做的事情,试试这个:

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

Source

【讨论】:

  • 啊,是的,你完全正确。非常感谢您的帮助
  • 我的错,我编辑了答案,写之前没有检查
  • 我现在得到Thu Sep 20 17:00:00 CEST+0200 2018,其中包括所需的偏移量(由于问题中的解析不正确,时间仍然不正确)。
  • 这很奇怪,doc 明确指出,'zzz' 表示时区和偏移量,但使用它们实际上不起作用,我再次更新了答案(我知道,它很烂,因此甚至我本人同意您的回答,但Instant 是在 26 年引入的,我们将不得不使用 backports),我已经对其进行了更新以支持 ISO 8601 格式,并且这次我已经在实际系统上对其进行了测试,这是可行的:) 是的,旧的日期时间课程很糟糕ref
【解决方案3】:

所以只是在这里添加一些更新。人们提供的答案有很大帮助,我现在有一个代码可以完全满足我的要求,但是其中一个 cmets 中的“遗留”一词让我觉得可能有一种更好、更持久的方法。以下是代码中当前发生的情况。

1) 我获取了一个带有字符串 utc 日期的足球赛程,格式为 2018-09-22T11:30:00Z

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 日期,无论设备如何,它都会返回 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 时,我已经检查了固定装置,它仍然可以正常工作。我试图确保一切都按照格林威治标准时间完成,以便它可以在任何地方工作。我不能确定会是这样。有人看到这种方法有任何缺陷吗?可以改进吗?

编辑:这是我希望简单准确地代表我正在做的代码的 sn-p。

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;
    }

}

}

【讨论】:

  • 我认为你必须对convertToGmt(或设备时区设置)做一些你没有告诉我们的事情,以便生成GMT+01:00而不考虑设备。 Date 对象没有形式,只要正确,总能准确比较。这也意味着获取当前时间就像new Date() 一样简单(虽然晦涩难懂,但并不是说它给出了当前时间)。
  • 您检索到的字符串中的 11:30 是 UTC(认为是 GMT),因此您的观察是正确的,这对应于 12:30 GMT+01:00。不过,您对Z 的解析仍然存在缺陷,因此我不明白您是如何获得正确时间的。
  • 您想要传统课程还是长期课程?如果您可以假设 API 级别 26,那么毫无疑问。如果不能,有利有弊,你要依赖外部库吗?我自己没有 Android 经验,但他们说这个库是坚如磐石的,由也开发 java.time 的人开发,直到将来的某个时候你才能假设 API 级别 26。对于非常简单的数据和时间操作我可以理解人们很想没有它,但你的只是更复杂,更清晰的 java.time 代码很有价值。
  • 我无法编译您的 DateSorter,但我已修复该问题。在您的模型类 Fixture 类中,我建议使用日期时间类作为夹具时间(如果您坚持使用过时的类,最好使用 InstantDate)。不是String。一方面,排序变得更加简单。 convertUtcDate.setTimeZone(TimeZone.getTimeZone("GMT")); 弥补了将 Z 解析为文字的危险,但我发现它令人困惑,并且如果您的 API 级别允许(当然,ThreeTenABP 确实允许),我仍然更喜欢将其解析为偏移量。在夹具中我得到Mon Sep 24 13:30:00 CEST 2018,没有偏移?
  • 如果您不介意我问,您在哪里?我想知道输出的时区是否特定于位置
猜你喜欢
  • 1970-01-01
  • 2018-08-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-20
  • 2011-10-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多