0

I am retrieving a String called date in the form 2018-09-20T17:00:00Z for example and converting it to a Date in the format Thu Oct 20 17:00:00 GMT+01:00 2018 using

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

On different devices however I am getting different results. One gives Thu Oct 20 17:00:00 BST 2018 (British Summer Time, equivalent to GMT+01:00) but this proves problematic later on. Is there a way to ensure dates are formatted in terms of a GMT offset i.e. GMT+01:00 instead of BST?

  • Can you post your formatting code as well? – MadScientist Sep 20 '18 at 17:21
  • Ok I have updated the post – Jack Heslop Sep 20 '18 at 17:31
  • Are you not calling `dateConvert.format(...)` anywhere? – MadScientist Sep 20 '18 at 17:33
  • I'm not no. Should I be? The date I retrieve at the start is a String, not a Date, so I parse the String to obtain a Date – Jack Heslop Sep 20 '18 at 17:36
  • Yep, wait, answering this in detail. – MadScientist Sep 20 '18 at 17:40
  • Ok thanks, appreciate it – Jack Heslop Sep 20 '18 at 17:41
  • As an aside consider not using `SimpleDateFormat` and `Date`. These classes are long outdated, and the former in particular notorioiusly troublesome. You may add [the ThreeTenABP library](https://github.com/JakeWharton/ThreeTenABP) to your project in order to use [java,time, the modern Java date and time API,](https://docs.oracle.com/javase/tutorial/datetime/) instead. It is so much nicer to work with. – Ole V.V. Sep 20 '18 at 19:24
  • 1
    FYI, the troublesome old date-time classes such as `java.util.Date`, `java.util.Calendar`, and `java.text.SimpleDateFormat` are now legacy, supplanted by the [*java.time*](https://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes. Most of the *java.time* functionality is back-ported to Java 6 & Java 7 in the [***ThreeTen-Backport***](http://www.threeten.org/threetenbp/) project. Further adapted for earlier Android in the [***ThreeTenABP***](https://github.com/JakeWharton/ThreeTenABP) project. See [*How to use ThreeTenABP…*](http://stackoverflow.com/q/38922754/642706). – Basil Bourque Sep 21 '18 at 01:53

3 Answers3

3

java.time

    Instant convertInstant = Instant.parse(date);

An Instant (just like a Date) represents a point in time independently of time zone. So you’re fine. As an added bonus your String of 2018-09-20T17:00:00Z is in the ISO 8601 format for an instant, so the Instant class parses it without the need for specifying the format.

EDIT: To format it into a human readable string in British Summer Time with unambiguous UTC offset use for example:

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

This code snippet printed:

Thu Sep 20 18:00:00 +0100 2018

18:00 is the correct time at offset +01:00. The Z at the end of the original string means offset zero, AKA “Zulu time zone”, and 17 at offset zero is the same point in time as 18:00 at offset +01:00. I took over the format pattern string from your own answer.

EDIT 2

I wanted to present to you my suggestion for rewriting the Fixture class from your own answer:

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

}

This class has a natural ordering (namely by date and time) in that it implements Comparable, meaning you no longer need your DateSorter class. A few lines of code to demonstrate the use of the new getXx methods:

    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());

When I ran this snippet in Europe/London time zone I got:

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

So the user gets the date and time with his or her own offset from UTC as I think you asked for. Trying the same snippet in Europe/Berlin time zone:

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

We see that the user in Germany is told that the match is at 13:30 rather than 12:30, which agrees with his or her clock. The date to be persisted in Firebase is unchanged, which is also what you want.

What went wrong in your code

There are two bugs in your format pattern string, yyyy-MM-dd'T'hh:mm:ss'Z':

  • Lowercase hh is for hour within AM or PM from 01 through 12 and only meaningful with an AM/PM marker. In practice you will get the correct result except when parsing an hour of 12, which will be understood as 00.
  • By parsing Z as a literal you are not getting the UTC offset information from the string. Instead SimpleDateFormat will use the time zone setting of the JVM. This obviously differs from one device to the other and explains why you got different and conflicting results in different devices.

The other thing going on in your code is the peculiar behaviour of Date.toString: this method grabs the JVM’s time zone setting and uses it for generating the string. So when one device is set to Europe/London and another to GMT+01:00, then equal Date objects will be rendered differently on those devices. This behaviour has confused many.

Question: Can I use java.time on Android?

Yes, java.time works nicely on 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, I’m told) the modern API comes built-in.
  • In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310; see the links at the bottom). The code above was developed and run with org.threeten.bp.Duration from the backport.
  • On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

Links

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • Thank you for your very thorough answer. I had not quite appreciated the difference between `hh` and `HH` and that has proved to be very helpful. – Jack Heslop Sep 21 '18 at 22:00
  • Please see my latest answer. I would appreciate your thoughts on it as you appear to have a very good grasp of dates and times in Java whereas I do not haha! – Jack Heslop Sep 21 '18 at 22:37
  • You may want to post runnable code in your answer (akin to a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve)), and I’ll take one more look after the weekend. – Ole V.V. Sep 22 '18 at 04:58
1

You are just doing step 1 of a two step process:

  1. Parse the date to convert it to a Date object
  2. Take this parsed Date object and format it using SimpleDateFormat again.

So, you've done step one correctly, here's what you have to do with step 2, try this:

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

Source

MadScientist
  • 2,134
  • 14
  • 27
  • Ahhh yes you're absolutely right. Thank you very much for your help – Jack Heslop Sep 20 '18 at 17:55
  • My bad, ive edited the answer, didnt check before writing – MadScientist Sep 21 '18 at 07:15
  • I now get `Thu Sep 20 17:00:00 CEST+0200 2018`, which includes an offset as desired (and still with the incorrect time due to incorrect parsing in the question). – Ole V.V. Sep 21 '18 at 11:07
  • 1
    This is weird, the [doc](https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) clearly states, 'zzz' for timezone and offset, but using them actually doesnt work, i've updated the answer again (i know, it sucks, hence even i myself agree to your answer but `Instant` was introduced in 26, we'll have to use backports), i've updated it to support ISO 8601 format, and i've tested it this time on an actual system, this works :) And yes old date-time classes are bad [ref](https://stackoverflow.com/questions/4542679/java-time-zone-when-parsing-dateformat) – MadScientist Sep 21 '18 at 11:39
0

So just to add a bit of an update here. The answers that people have provided have helped massively and I now have a code that does exactly what I want, but the term 'legacy' in one of the comments makes me feel that there may be a better and longer-lasting way. Here's what currently happens in the code.

1) I fetch a football fixture which comes with a String utc date in the form 2018-09-22T11:30:00Z

2) I then parse the date using SimpleDateFormat convertUtcDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); and convertUtcDate.setTimeZone(TimeZone.getTimeZone("GMT"));

3) I then get the current time using currentTime = Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTime(); and compare the two using if(convertedDate.after(currentTime)) to find a team's next fixture. At this point I have found that a device will have these two dates in the same form, either with BST or GMT+01:00 but either way the two dates can be accurately compared.

4) I then format the date so it is in terms of a GMT offset using SimpleDateFormat convertToGmt = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US); and String dateString = convertToGmt.format(convertedDate);

5) For the utc date in 1) this returns Sat Sep 22 12:30:00 GMT+01:00 2018 regardless of the device. Notice that the time is different to the utc date. Not quite sure why this is the case (could be because the guy running the API is based in Germany which is an hour ahead of me here in England) but the important thing is that this time is correct (it refers to the Fulham - Watford game tomorrow which is indeed at 12:30 BST/GMT+01:00).

6) I then send this String along with a few other bits of information about the fixture to Firebase. It is important that the date be in the form GMT+01:00 rather than BST at this point because other devices may not recognise the BST form when they read that information.

7) When it comes to calling that information back from Firebase, I convert it back to a date by parsing it using SimpleDateFormat String2Date = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy", Locale.US); and then I can arrange the fixtures in chronological order by comparing their dates.

I would just like to reiterate that this method works. I have checked for fixtures when the time zone in England changes back to GMT+00:00 and it still works fine. I have tried to make sure that everything is done in terms of GMT so that it would work anywhere. I cannot be sure though that this would be the case. Does anyone see any flaws in this method? Could it be improved?

Edit: Here is a snippet of the code which I hope simply and accurately represents what I am doing.

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

}

}

  • I think you must be doing something to `convertToGmt` (or to the device time zone setting) that you haven’t told us in order to produce `GMT+01:00` regardless of the device. `Date` objects don’t have forms, so as long as they are correct, they can always be compared accurately. This also means that getting current time is as simple as `new Date()` (though cryptic, it doesn’t really say that this gives the current time). – Ole V.V. Sep 22 '18 at 04:55
  • The 11:30 in the string you have retrieved is in UTC (think of as GMT), so your observation is correct, that this corresponds to 12:30 GMT+01:00. Your parsing of the `Z` is still flawed, though, so how you managed to get the correct time I havem’t understood. – Ole V.V. Sep 22 '18 at 05:03
  • Whether you want the legacy or the long-lasting classes? If you could assume API level 26, there’d be no question. If you cannot, there are pros and cons, do you want to depend on an external library? I haven’t got Android experience myself, but they say this library is rock solid, developed by the folks that also developed java.time, and it’s only until some time in the future you can assume API level 26. For very simple data and time operations I can understand that people are tempted to do without it, but yours are just that more complex that the clearer code with java.time is valuable. – Ole V.V. Sep 22 '18 at 05:09
  • I couldn’t get your `DateSorter` to compile, but I fixed that. In your model class, the `Fixture` class, I recommend using a date-time class for the fixture time (preferably `Instant`, `Date` if you insist on the outdated classes). Not a `String`. For one thing sorting gets much simpler. `convertUtcDate.setTimeZone(TimeZone.getTimeZone("GMT"));` makes up for the dangerous parsing of `Z` as a literal, but I find it confusing and would still prefer parsing as an offset if your API level permits (ThreeTenABP does permit, of course). In the fixture I get `Mon Sep 24 13:30:00 CEST 2018`, no offset? – Ole V.V. Sep 24 '18 at 09:59
  • Where are you based, if you don't mind me asking? I'm wondering if the outputted time zone is location-specific – Jack Heslop Sep 25 '18 at 13:35
  • Sure, I’m in Europe/Copenhagen time zone, and you are correct, CEST is for Central European Summer Time, which is what we use in my time zone until October 27. – Ole V.V. Sep 25 '18 at 13:54
  • I see. The only reason I have been converting to and from a string is that I send and fetch the data to and from Firebase, but I've noticed that a value can be stored in Firebase as a Timestamp which may lead me down a better path. I'll look into it and report back. Thanks again for all your help – Jack Heslop Sep 26 '18 at 09:02
  • Yeah Timestamp was the way to go. Means I can keep it in a Timestamp form which is easily convertible to a date. I've also experimented with changing the time zone on my device and it handles this perfectly. – Jack Heslop Sep 26 '18 at 14:42
  • I had a couple of suggestions for your Fixture class. See the EDIT 2 in my answer, please. – Ole V.V. Sep 27 '18 at 14:49