SimpleDateFormat显示错误的本地时间

huangapple go评论77阅读模式
英文:

SimpleDateFormat shows wrong local Time

问题

我想将一个字符串与当前的时间和日期存储到一个数据库(SQLite)中,用于一个 Android 应用。为此,我使用了 SimpleDateFormat。不幸的是,它显示的时间不正确。我尝试了两个选项。

第一个选项(来自 https://stackoverflow.com/questions/37747467/simpledateformat-with-timezone/37747640

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.getDefault());
sdf.format(new Date());

第二个选项(来自 https://stackoverflow.com/questions/19112357/java-simpledateformatyyyy-mm-ddthhmmssz-gives-timezone-as-ist

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss'Z'");
sdf2.setTimeZone(TimeZone.getTimeZone("CEST"));

在这两种情况下,时间都是错误的。它不是我笔记本电脑或手机显示的本地时间,而是输出时间早了 2 小时。我该如何更改?我希望拥有柏林(CEST)的当前时间,这也显示在我的电脑上。我感谢每一个评论。

英文:

I want to store a string into a databse (SQLite) for an Android App with the current time and date. For that purpose I am using SimpleDateFormat. Unfortunately it does not show the correct time when. I tried two options.

First Option (from https://stackoverflow.com/questions/37747467/simpledateformat-with-timezone/37747640)

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.getDefault());
        sdf.format(new Date());

Second option (from https://stackoverflow.com/questions/19112357/java-simpledateformatyyyy-mm-ddthhmmssz-gives-timezone-as-ist)

    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss'Z'");
    sdf2.setTimeZone(TimeZone.getTimeZone("CEST"));

In both cases the time is just wrong. It is not the local time that my laptop or phone is showing but the output time is 2 hours earlier. How can I change that? I would like to have the current time of Berlin (CEST) that is also shown on my computer. I appreciate every comment.

答案1

得分: 6

使用Europe/Berlin代替CEST,您将获得预期结果。

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

public class Main {
    public static void main(String[] args) {
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
        sdf2.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"));
        System.out.println(sdf2.format(new Date()));
    }
}

输出:

2020-09-27 18:38:04 +0200

一些建议:

我建议您从过时且容易出错的java.util日期时间 API 和 SimpleDateFormat 切换到现代java.time 日期时间 API,以及相应的格式化 API(位于包 java.time.format 中)。从 教程:日期时间 了解更多关于现代日期时间 API 的信息。如果您的 Android API 级别仍不符合 Java 8,请查看 通过 desugaring 可用的 Java 8+ API 以及 如何在 Android 项目中使用 ThreeTenABP

使用现代日期时间 API:

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));

        // 默认格式
        System.out.println(zdt);

        // 一些自定义格式
        System.out.println(zdt.format(DateTimeFormatter.ofPattern("EEEE dd uuuu hh:mm:ss a z")));
    }
}

输出:

2020-09-27T18:42:53.620168+02:00[Europe/Berlin]
Sunday 27 2020 06:42:53 pm CEST

现代 API 会发出警告,而传统 API 可能会出错:

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("CEST"));
        // ...
    }
}

输出:

Exception in thread "main" java.time.zone.ZoneRulesException: Unknown time-zone ID: CEST
    at java.base/java.time.zone.ZoneRulesProvider.getProvider(ZoneRulesProvider.java:279)
    at java.base/java.time.zone.ZoneRulesProvider.getRules(ZoneRulesProvider.java:234)
    at java.base/java.time.ZoneRegion.ofId(ZoneRegion.java:120)
    at java.base/java.time.ZoneId.of(ZoneId.java:408)
    at java.base/java.time.ZoneId.of(ZoneId.java:356)
    at Main.main(Main.java:6)

正如您所见,您在这种情况下会收到一个异常,而SimpleDateFormat会给您不期望的结果,如下所示:

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

public class Main {
    public static void main(String[] args) {
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
        sdf2.setTimeZone(TimeZone.getTimeZone("CEST"));
        System.out.println(sdf2.format(new Date()));
    }
}

输出:

2020-09-27 16:47:45 +0000

您可能想知道这个不期望的结果是什么。答案是:当SimpleDateFormat不理解一个时区时,它会回退(默认)到GMT(与UTC相同),即在这种情况下它忽略了CEST并应用了GMT(在我看来不是一个好的特性 😊)。

英文:

Use Europe/Berlin instead of CEST and you will get the expected result.

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

public class Main {
	public static void main(String[] args) {
		SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
		sdf2.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"));
		System.out.println(sdf2.format(new Date()));
	}
}

Output:

2020-09-27 18:38:04 +0200

A piece of advice:

I recommend you switch from the outdated and error-prone java.util date-time API and SimpleDateFormat to the modern java.time date-time API and the corresponding formatting API (package, java.time.format). Learn more about the modern date-time API from Trail: Date Time. If your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Using the modern date-time API:

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class Main {
	public static void main(String[] args) {
		ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));

		// Default format
		System.out.println(zdt);

		// Some custom format
		System.out.println(zdt.format(DateTimeFormatter.ofPattern("EEEE dd uuuu hh:mm:ss a z")));
	}
}

Output:

2020-09-27T18:42:53.620168+02:00[Europe/Berlin]
Sunday 27 2020 06:42:53 pm CEST

The modern API will alert you whereas legacy API may failover:

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
	public static void main(String[] args) {
		ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("CEST"));
		// ...
	}
}

Output:

Exception in thread "main" java.time.zone.ZoneRulesException: Unknown time-zone ID: CEST
	at java.base/java.time.zone.ZoneRulesProvider.getProvider(ZoneRulesProvider.java:279)
	at java.base/java.time.zone.ZoneRulesProvider.getRules(ZoneRulesProvider.java:234)
	at java.base/java.time.ZoneRegion.ofId(ZoneRegion.java:120)
	at java.base/java.time.ZoneId.of(ZoneId.java:408)
	at java.base/java.time.ZoneId.of(ZoneId.java:356)
	at Main.main(Main.java:6)

As you can see, you get an exception in this case whereas SimpleDateFormat will give you undesirable result as shown below:

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

public class Main {
	public static void main(String[] args) {
		SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
		sdf2.setTimeZone(TimeZone.getTimeZone("CEST"));
		System.out.println(sdf2.format(new Date()));
	}
}

Output:

2020-09-27 16:47:45 +0000

You might be wondering what this undesirable result refers to. The answer is: when SimpleDateFormat doesn't understand a time-zone, it failovers (defaults) to GMT (same as UTC) i.e. it has ignored CEST and applied GMT in this case (not a good feature IMHO 😊).

答案2

得分: 4

ISO 8601

假设你的SQLite没有datetime数据类型,我建议你使用ISO 8601格式,这是国际标准,将你的日期时间存储为SQLite中的字符串。接下来,考虑使用java.time,现代Java日期和时间API,来处理你的日期和时间工作。这两个建议很好地配合在一起。通常的建议是将日期和时间存储为UTC,但我理解你更喜欢欧洲/柏林时间。

ZoneId databaseTimeZone = ZoneId.of("Europe/Berlin");

ZonedDateTime now = ZonedDateTime.now(databaseTimeZone);
String databaseTime = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

System.out.println(databaseTime);

上述代码的输出(刚刚运行):

> 2020-09-27T15:54:21.53+02:00

我特意在字符串中包含了与UTC的偏移量。这将使任何检索该字符串的人都能将时间转换为UTC或其首选的时区。如果用户前往印度参观泰姬陵并在那里检索数据,转换为印度标准时间也没有问题。偏移量还可以消除十月份夜间在柏林从夏时制(DST)切换到标准时间时以及相同的时钟时间重复出现时的歧义。更改之前的时间偏移量为+02:00,更改后的时间偏移量为+01:00。

如何改变格式(?)

编辑:如果你坚持使用自己的格式以满足信息和人类可读性的要求,请为此构建一个格式化器。ZonedDateTime已经包含了在你选择的时区中的时间,因此在你格式化它时,时间也将是你选择的时间:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
String databaseTime = now.format(formatter);

现在的结果是:

> 2020-09-27 16:22:23 +0200

进一步编辑:既然人类可读性是该列的唯一要求,那就全力以赴,并使用Java预定义的本地化格式,例如:

DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
        .withLocale(Locale.GERMAN);
> 27. September 2020 19:06:03 MESZ

如果对你来说太长了,可以改用FormatStyle.MEDIUM

进一步进一步编辑:为什么呢?问题是27. September 2020 19:06:03 MESZ是否比2020-09-27 16:22:23 +0200更容易阅读和正确理解。你应该尽可能地让自己轻松些。包含偏移量+0200是有道理的,因为它是明确的,而像MESZ这样的时区缩写不一定保证是明确的(许多时区缩写是有歧义的)。

代码出了什么问题?

你可能是在计算机上运行代码,该计算机的时区设置为UTC(或其他当前比柏林时间晚两小时的时区)。在你的第二个代码片段中,你试图通过将格式化器的时区设置为CEST(中欧夏令时)来弥补这一事实。你所做的方式不是你想要的,而且也不起作用。这两者都与CEST不是一个时区有关。CEST比UTC快两个小时,如果它起作用的话,在柏林只比UTC快1个小时的标准时间期间,你会得到比UTC快两个小时的时间,也就是错误的时间。由于CEST不是一个时区,TimeZone类不会将其识别为时区。这与TimeZone类一样令人困惑:它不是拒绝,而是默默地给你GMT时间,因此你无法得到想要的结果。我真的建议避免使用那个类。柏林的正确时区标识符是Europe/Berlin,这也是我在我的代码中使用的。时区标识符采用区域/城市的格式。

问题:java.time需要Android API级别26吗?

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

  • 在Java 8及更高版本以及较新的Android设备(API级别26以上)上,现代API内置其中。
  • 在非Android Java 6和7中,可以使用ThreeTen Backport,即现代类的后移(JSR 310的ThreeTen)。请参阅底部的链接。
  • 在较旧的Android设备上,可以使用desugaring或ThreeTen Backport的Android版本。它被称为ThreeTenABP。在后一种情况下,请确保从org.threeten.bp及其子包导入日期和时间类。

链接

英文:

ISO 8601

Assuming that your SQLite hasn’t got a datetime datatype I recommend that you use ISO 8601 format, the international standard, for storing your date-times as strings to SQLite. Next, consider using java.time, the modern Java date and time API, for your date and time work. The two suggestions go nicely hand in hand. Common recommendations say to store date and time in UTC, but I understand that you prefer Europe/Berlin time.

	ZoneId databaseTimeZone = ZoneId.of("Europe/Berlin");
	
	ZonedDateTime now = ZonedDateTime.now(databaseTimeZone);
	String databaseTime = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
	
	System.out.println(databaseTime);

Output from the above when running just now:

> 2020-09-27T15:54:21.53+02:00

I have on purpose included the offset from UTC in the string. This will allow anyone retrieving the string to convert the time to UTC or the time zone of their preference. If the user travels to India to see Taj Mahal and retrieves the data there, converting to India Standard Time is no problem. The offset also disambiguates times in the night in October when Berlin changes from summer time (DST) to standard time and the same clock times repeat. Times before the change will have offset +02:00, times after the change will have +01:00.

> How can I change the format(?)

Edit: If you insist on your own format for information and human readability, build a formatter for that. The ZonedDateTime already has the time in your chosen time zone, so that time is also the one you will have when you format it:

	DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
	String databaseTime = now.format(formatter);

Now the result is:

> 2020-09-27 16:22:23 +0200

Further edit: Since human readability is the only requirement for that column, go all-in on that and use java’s predefined localized format, for example:

	DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
			.withLocale(Locale.GERMAN);

> 27. September 2020 19:06:03 MESZ

If it’s too long for you, use FormatStyle.MEDIUM instead.

Further further edit: And why? The question is whether 27. September 2020 19:06:03 MESZ is easier to read and understand correctly than 2020-09-27 16:22:23 +0200. You should make it as easy for yourself as you reasonably can. There is a point in including the offset, +0200, though, since it is unambiguous whereas a time zone abbreviation like MESZ is not guaranteed to be (many time zone abbreviations are ambiguous).

What went wrong in your code?

You are probably running your code on a computer with its time zone set to UTC (or some other time zone that is currently two hours behind Berlin time). In your second snippet you are trying to make up for this fact by setting the time zone of you formatter to CEST (Central European Summer Time). The way you are doing that is not what you want, and it also does not work. Both have to do with the fact that CEST is not a time zone. CEST is two hours ahead of UTC, and if it had worked, you would have got two hours ahead of UTC also during the standard time of year where Berlin is only 1 hour ahead of UTC, that is, the wrong time. Since CEST is not a time zone, TimeZone does not recognize it as a time zone. And this is as confusing as the TimeZone class is: instead of objecting, it tacitly gives you GMT, so you have got nowhere. I really recommend avoiding using that class. The correct time zone identifier for Berlin is Europe/Berlin, the one I am also using in my code. Time zone identifiers come in the region/city format.

Question: Doesn’t java.time require Android API level 26?

java.time works nicely on both older and newer Android devices. It just requires at least Java 6.

  • In Java 8 and later and on newer Android devices (from API level 26) the modern API comes built-in.
  • In non-Android Java 6 and 7 get the ThreeTen Backport, the backport of the modern classes (ThreeTen for JSR 310; see the links at the bottom).
  • On older Android either use desugaring or the Android edition of ThreeTen Backport. It’s called ThreeTenABP. In the latter case make sure you import the date and time classes from org.threeten.bp with subpackages.

答案3

得分: 1

以下是翻译好的内容:

我在一周前遇到了相同的问题,我找出了问题所在是在时区设置方面。

如果您将日期作为字符串获取,并且需要将其格式化为另一种格式,请使用以下代码

public String getCalendarDate(String inputDate){
        Date date = getDateFromSource(inputDate);
        SimpleDateFormat formatter = new SimpleDateFormat("EEEE, d MMMM yyyy", Locale.getDefault());
        formatter.setTimeZone(TimeZone.getDefault());
        return formatter.format(date);
    }

    Date getDateFromSource(String apiDate){
        Date newFormattedDate = null;
        SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
        parser.setTimeZone(TimeZone.getTimeZone("UTC"));
        try {
             newFormattedDate = parser.parse(apiDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return newFormattedDate;
    }

getDateFromSource函数中,将日期格式更改为源格式;而在getCalendarDate函数中,将格式更改为您所需的格式。

如果您已经有了Date对象,则可以忽略getDateFromSource函数,并直接放在第二个函数中。

对于使用Kotlin的人,以下是等效的代码

    fun getCalendarDate(apiDate: String): String{
        val date = getDateFromApi(apiDate)
        val formatter = SimpleDateFormat("EEEE, d MMMM yyyy", Locale.getDefault())
        formatter.timeZone = TimeZone.getDefault()
        return formatter.format(date)
    }

    private fun getDateFromApi(apiDate: String) :Date{
        val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
        parser.timeZone = TimeZone.getTimeZone("UTC")
        return parser.parse(apiDate)!!
    }
英文:

Well, i faced the same issue a week ago and I figured out that the problem is in the TimeZone settings

If you are getting the date as a string and you need to format it to another format use the code below

public String getCalendarDate(String inputDate){
        Date date = getDateFromSource(inputDate);
        SimpleDateFormat formatter = new SimpleDateFormat("EEEE, d MMMM yyyy", Locale.getDefault());
        formatter.setTimeZone(TimeZone.getDefault());
        return formatter.format(date);
    }

    Date getDateFromSource(String apiDate){
        Date newFormattedDate = null;
        SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
        parser.setTimeZone(TimeZone.getTimeZone("UTC"));
        try {
             newFormattedDate = parser.parse(apiDate);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return newFormattedDate;
    }

In the getDateFromSource function change the date format to the source format, while in the getCalendarDate function, change the format to your required format.

If you already have the Date object, you can ignore the getDateFromSource function and put it directly in the second one

For those who use Kotlin this is the equivalent code

    fun getCalendarDate(apiDate: String): String{
        val date = getDateFromApi(apiDate)
        val formatter = SimpleDateFormat("EEEE, d MMMM yyyy", Locale.getDefault())
        formatter.timeZone = TimeZone.getDefault()
        return formatter.format(date)
    }

    private fun getDateFromApi(apiDate: String) :Date{
        val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
        parser.timeZone = TimeZone.getTimeZone("UTC")
        return parser.parse(apiDate)!!
    }

huangapple
  • 本文由 发表于 2020年9月27日 21:08:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/64088810.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定