4
votes

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 SimpleDateFormat with TimeZone)

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

Second option (from Java SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") 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.

3
Why not store the current time as unix timestamp? Then convert the ms into a date string when needed. Saves some bytes and can be converted to any other timezone without much hassle. - Viatcheslav Ehrmann
Store System.currentTimeMillis(); parsed as a String. Use it along with a Calendar to display it as text again. If you'd like to know how to do this, Please comment... I'll post an answer. - Vishnu
Thanks Viatcheslav and Vishnu for your comments. I already have an entry in the database with the current time in milliseconds. This should be a second entry. The first entry (in milliseconds) is used for quering entries between two time points. As the entry in milliseconds in not 'readable' for humans, I would also like to have a second readable entry in the database - VanessaF
@VanessaF I'll post an answer now to make human-readable time from MilliSeconds.. - Vishnu
As an aside consider throwing away the long outmoded and notoriously troublesome SimpleDateFormat and friends. See if you either can use desugaring or add ThreeTenABP to your Android project, in order to use java.time, the modern Java date and time API. It is so much nicer to work with. - Ole V.V.

3 Answers

4
votes

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 😊).

4
votes

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);
  1. 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.

Links

1
votes

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)!!
    }