39

I would appreciate any help with finding bug for this exception:

java.text.ParseException: Unparseable date: "2007-09-25T15:40:51.0000000Z"

and following code:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
Date date = sdf.parse(timeValue);
long mills = date.getTime();
this.point.time = String.valueOf(mills);

It throws expcetion with Date date = sdf.parse(timeValue); .

timeValue = "2007-09-25T15:40:51.0000000Z"; , as in exception.

Thanks.

Jacob
  • 14,949
  • 19
  • 51
  • 74
  • Do you even need to parse for `.SSSZ`? If all you want is date or time, then remove the `.SSSZ`. – IgorGanapolsky Oct 14 '15 at 20:27
  • 2
    For new readers to this question I recommend you don’t use `SimpleDateFormat` and `Date`. Those classes are poorly designed and long outdated, the former in particular notoriously troublesome. Instead just use `Instant` from [java.time, the modern Java date and time API](https://docs.oracle.com/javase/tutorial/datetime/). – Ole V.V. Dec 04 '21 at 09:19
  • In addition to what @OleV.V. has suggested, note that ['Z' is not the same as Z](https://stackoverflow.com/a/67953075/10819573). – Arvind Kumar Avinash Nov 16 '22 at 22:34

3 Answers3

89

Z represents the timezone character. It needs to be quoted:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
Reimeus
  • 158,255
  • 15
  • 216
  • 276
  • 3
    Or possibly use X instead of Z so that Z is accepted as an ISO8601 timezone, for which "Z" is parsed as the UTC time zone designator – DNA Nov 23 '13 at 22:30
  • Using X works for me, BUT seems to require an exact number of S (millisecond) characters in the patterns, which is strange - see my answer... – DNA Nov 23 '13 at 22:41
  • It's in the [javadoc](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) _Text can be quoted using single quotes (') to avoid interpretation_ – Reimeus Nov 23 '13 at 23:49
  • 1
    @Reimeus This solution didn't work for me. I tried the `'Z'`, and it didn't get parsed. It only worked when I removed the Z. – IgorGanapolsky Oct 14 '15 at 21:50
  • 1
    This answer is wrong in two ways: (1) `Z` means UTC. Quoting `Z` causes `SimpleDateFormat` to use the JVM default time zone instead for an error of up to 14 hours. (2) `SSS` means milliseconds. The string in the question `2007-09-25T15:40:51.0000000Z`, has 10-milliionths of seconds. As long as they are 0, the result is the same of course, but your formatter parses for example `2007-09-25T15:40:51.5000000Z` into Tue Sep 25 17:04:11 CEST 2007, an error of more than an hour on top of the incorrect time zone. – Ole V.V. Aug 18 '22 at 17:12
  • @DNA Putting *an exact number of S (millisecond) characters* does not help. It makes no difference. You are correct that there is an error here, as I said. – Ole V.V. Aug 18 '22 at 17:23
  • ['Z' is not the same as Z](https://stackoverflow.com/a/67953075/10819573) – Arvind Kumar Avinash Nov 16 '22 at 21:59
8

(Answer now extensively revised, thanks for the corrections in the comments)

In Java 7 you can use the X pattern to match an ISO8601 timezone, which includes the special Z (UTC) value.

The X pattern also supports explicit timezones, e.g. +01:00

This approach respects the timezone indicator correctly, and avoids the problem of treating it merely as a string, and thus incorrectly parsing the timestamp in the local timezone rather than UTC or whatever.

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
Date date = sdf.parse("2007-09-25T15:40:51Z");
Date date2 = sdf.parse("2007-09-25T15:40:51+01:00");

This can also be used with milliseconds:

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
Date date3 = sdf2.parse("2007-09-25T15:40:51.500Z");

However, as others have pointed out, your format has 7-digit fractional seconds, which are presumably tenth-microseconds. If so, SimpleDateFormat cannot handle this, and you will get incorrect results, because each 0.1 microsecond will be interpreted as a millisecond, giving a potential overall error of up to 10,000 seconds (several hours).

In the extreme case, if the fractional second value is 0.9999999 seconds, that will be incorrectly interpreted as 9999999 milliseconds, which is about 167 minutes, or 2.8 hours.

// Right answer, error masked for zero fractional seconds 
Date date6 = sdf2.parse("2007-09-25T15:40:51.0000000Z");
// Tue Sep 25 15:40:51 GMT 2007

// Error - wrong hour
// Should just half a second different to the previous example
Date date5 = sdf2.parse("2007-09-25T15:40:51.5000000Z");
// Tue Sep 25 17:04:11 GMT 2007

This error is hidden when the fractional seconds are zero, as in your example, but will manifest whenever they are nonzero.

This error can be detected in many cases, and its impact reduced, by turning off "lenient" parsing which by default will accept a fractional part of more than one second and carry it over to the seconds/minutes/hours parts:

sdf2.setLenient(false);
sdf2.parse("2007-09-25T15:40:51.5000000Z");
// java.text.ParseException: Unparseable date: "2007-09-25T15:40:51.5000000Z"

This will catch cases where the millis value is more than 999, but does not check the number of digits, so it is only a partial and indirect safeguard against millis/microseconds mismatches. However, in many real-world datasets this will catch a large number of errors and thus indicate the root problem, even if some values slip through.

I recommend that lenient parsing is always disabled unless you have a specific need for it, as it catches a lot of errors that would otherwise be silently hidden and propagated into downstream data.

If your fractional seconds are always zero, then you could use one of the solutions here, but with the risk that they will NOT work if the code is later used on non-zero fractional seconds. You may wish to document this and/or assert that the value is zero, to avoid later bugs.

Otherwise, you probably need to convert your fractional seconds into milliseconds, so that SimpleDateFormat can interpret them correctly. Or use one of the newer datetime APIs.

DNA
  • 42,007
  • 12
  • 107
  • 146
  • `IllegalArgumentException: Unknown pattern character 'X'` – IgorGanapolsky Oct 14 '15 at 20:24
  • 1
    Igor - what version of Java gives that error? The 'X' pattern is [clearly documented](https://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) for Java 7, and works for me under Java 8 too. – DNA Oct 14 '15 at 22:13
  • I am using ThreeTenABP library in my Android project (Java 8). – IgorGanapolsky Oct 14 '15 at 22:13
  • 2
    @IgorGanapolsky sounds like you need to post a new question will full details of your code & environment – Reimeus Oct 16 '15 at 16:43
  • @IgorGanapolsky See [my answer](https://stackoverflow.com/a/70224524/5772882) (works with ThreeTenABP too). – Ole V.V. Dec 04 '21 at 09:45
  • You're quite right, will update answer later to explain the benefit of the X pattern (parsing ISO 8601 timezones other than 'Z') but without the millisecond confusion. – DNA Aug 19 '22 at 08:45
  • Thanks for the rewrite and improvement. Setting the formatter non-lenient does not always work as intended. For example it still parses `"2007-09-25T15:40:51.0000500Z"` with an incorrect result without any exception. Non-leniency causes it to check that the milliseconds are in the 0–999 range, not whether too many digits are present (or too few for that matter). – Ole V.V. Aug 20 '22 at 11:53
  • 1
    Agreed; I'll try to clarify what I meant by "in many cases" (i.e. the cases where millis > 999) – DNA Aug 23 '22 at 11:26
1

java.time

I recommend that you use java.time, the modern Java date and time API, for your date and time work. Your string is in ISO 8601 format and can be directly parsed by the java.time.Instant class without us specifying any formatter:

    String timeValue = "2007-09-25T15:40:51.0000000Z";
    
    Instant i = Instant.parse(timeValue);
    long mills = i.toEpochMilli();
    String time = String.valueOf(mills);
    
    System.out.println(time);

Output:

1190734851000

May use a formatter for output if desired

If we know for a fact that the millisecond value will never be negative, java.time can format it into a string for us. This saves the explicit conversion to milliseconds first.

private static final DateTimeFormatter EPOCH_MILLI_FORMATTER
        = new DateTimeFormatterBuilder().appendValue(ChronoField.INSTANT_SECONDS)
                .appendValue(ChronoField.MILLI_OF_SECOND, 3)
                .toFormatter(Locale.ROOT);

Now formatting is trivial:

    assert ! i.isBefore(Instant.EPOCH) : i;
    String time = EPOCH_MILLI_FORMATTER.format(i);

And output is still the same:

1190734851000

In particular if you need to format Instant objects to strings in more places in your program, I recommend the latter approach.

What went wrong in your code?

First of all, there is no way that SimpleDateFormat can parse 7 decimals of fraction of second correctly. As long as the fraction is zero, the result will happen to come out correct anyway, but imagine a time that is just one tenth of a second after the full second, for example, 2007-09-25T15:40:51.1000000Z. In this case SimpleDateFormat would parse the fraction into a million milliseconds, and your result would be more than a quarter of an hour off. For greater fractions the error could be several hours.

Second as others have said format pattern letter Z does not match the offset of Z meaning UTC or offset zero from UTC. This caused the exception that you observed. Putting Z in quotes as suggested in the accepted answer is wrong too since it will cause you to miss this crucial information from the string, again leading to an error of several hours (in most time zones).

Link

Oracle tutorial: Date Time explaining how to use java.time.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161