0

When using SimpleDateFormat to produce a string from a Firebase Firestore Timestamp object, I get an incorrect/nonsense result. Why?

I fabricate some Timestamps and send them to Firestore. I see expected results in the Firebase/Firestore console. I then get the douments from Firestore, retrieve the Timestamps from the documents, and convert them first to Date objects then to strings using SimpleDateFormat. The last step produces incorrect strings: specifically times "18:08:00" instead of "18:28:53" and "19:08:64" instead of "19:28:53" (Note: seconds > 59)

Firebase console shows the timestamps I expect:

eventTimestamp August 10, 2019 at 6:28:53 PM UTC+1
collectedTimestamp August 10, 2019 at 7:28:53 PM UTC+1

But the following code shows the problem (selectedMedEvent is an object created from the document retrieved, and getEventTimestamp() and getCollectedTimestamp() both return Firebase Timestamp objects):

Date eDate = selectedMedEvent.getEventTimestamp().toDate();
Date cDate = selectedMedEvent.getCollectedTimestamp().toDate();
String eString = selectedMedEvent.getEventTimestamp().toString();
String cString = selectedMedEvent.getCollectedTimestamp().toString();

SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:MM:SS");
String eventDateString = formatter.format(eDate);
String collectedDateString = formatter.format(cDate);
Log.d(TAG, "Event Date is '" + eDate + "' or " + eString + " but SimpleDateFormat gives '" + eventDateString + "'");
Log.d(TAG, "Collected Date is '" + cDate + "' or " + cString + " but SimpleDateFormat gives '" + collectedDateString + "'");

Logcat shows the two Date objects printed as expected (matching the Firestore console) but errors elsewhere:

Event Date is 'Sat Aug 10 18:28:53 GMT+01:00 2019' or Timestamp(seconds=1565458133, nanoseconds=0) but SimpleDateFormat gives '10/08/2019 18:08:00'
Collected Date is 'Sat Aug 10 19:28:53 GMT+01:00 2019' or Timestamp(seconds=1565461733, nanoseconds=648000000) but SimpleDateFormat gives '10/08/2019 19:08:64'

Note the non-zero nanoseconds. When I create the Timestamp the nanoseconds value is always 0.

What is this happening and how do I fix it?

Later: I have created a stand-alone routine that demonstrates the problem. I plugged in the seconds and nanoseconds values that the Logcat had given me and I get the same unexpected SimpleDateFormat strings. Code is:

private void showBug() {
    Timestamp ts1 = new Timestamp( 1565458133, 0);
    Timestamp ts2 = new Timestamp( 1565461733, 648000000);

    Date eDate = ts1.toDate();
    Date cDate = ts2.toDate();
    String eString = ts1.toString();
    String cString = ts2.toString();

    SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:MM:SS");
    String eventDateString = formatter.format(eDate);
    String collectedDateString = formatter.format(cDate);
    Log.d(TAG, "Event Date is '" + eDate + "' or " + eString + " but SimpleDateFormat gives '" + eventDateString + "'");
    Log.d(TAG, "Collected Date is '" + cDate + "' or " + cString + " but SimpleDateFormat gives '" + collectedDateString + "'");
}

Logcat output is identical to previously reported.

Acutetech
  • 3
  • 3
  • 1
    FYI, the terribly troublesome date-time classes such as [`java.util.Date`](https://docs.oracle.com/javase/10/docs/api/java/util/Date.html), [`java.util.Calendar`](https://docs.oracle.com/javase/10/docs/api/java/util/Calendar.html), and `java.text.SimpleDateFormat` are now [legacy](https://en.wikipedia.org/wiki/Legacy_system), supplanted by the [*java.time*](https://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes built into Java 8 and later. See [*Tutorial* by Oracle](https://docs.oracle.com/javase/tutorial/datetime/TOC.html). – Basil Bourque Aug 11 '19 at 01:50
  • Possible partial duplicate of [Converting String to Time using SimpleDateFormat issues](https://stackoverflow.com/questions/50449169/converting-string-to-time-using-simpledateformat-issues) – Ole V.V. Aug 21 '19 at 06:51

2 Answers2

3

tl;dr

Your direct problem is wrong codes in your formatting pattern. Your larger problem is using terrible legacy classes.

Convert from Firestore Timestamp (not to be confused with java.sql.Timestamp) to modern java.time.Instant.

Instant instant = Instant.of( timestamp.getSeconds() , timestamp.getNanoseconds() ) 

Example: Instant.of( 1_565_461_733L , 648_000_000L )

Convert from java.time.Instant to Firestore Timestamp.

new Timestamp( instant.getEpochSecond() , instant.getNano() )

Beware of data loss: Apparently Firestore is limited to microseconds rather than nanoseconds resolution. Why their own Timestamp class resolves to nanos is beyond my comprehension. (I do not use Firestore/Firebase.)

Formatting pattern incorrect

new SimpleDateFormat("dd/MM/yyyy HH:MM:SS");

You are using an incorrect formatting pattern. The pattern codes are case-sensitive. Read the class Javadoc, and search Stack Overflow as this has been covered thousands of times already. Search Stack Overflow before posting.

And you should not be using that class anyways.

java.time

The terrible date-time classes in Java such as SimpleDateFormat, Date, and Timestamp were supplanted years ago by the modern java.time classes. Both java.util.Date and java.sql.Timestamp were replaced by java.time.Instant (and by java.time.OffsetDateTime for JDBC work).

Unfortunately, it seems that Firestore has not yet been updated to java.time. Fortunately, you can convert to-and-fro between the legacy and modern classes. Look to new to… & from… methods found on the old classes.

java.time.Instant

You seem to have inputs of a count of whole seconds since the epoch reference of first moment of 1970 in UTC, plus fractional second as a count of nanoseconds. Pass those to a factory method in Instant.

Instant instant = Instant.ofEpochSecond ( 1_565_461_733L , 648_000_000L );

instant.toString(): 2019-08-10T18:28:53.648Z

Truncating

Timestore is limited to microseconds rather than the nanoseconds in java.time. So you may want to make a habit of truncating for consistency.

instant = instant.truncatedTo( ChronoUnit.MICROS ) ;

Firestore Timestamp class

Apparently Firestore has its own Timestamp class. It's constructor takes a java.util.Date object. So we could convert from java.time.Instant to java.util.Date and feed that to a Firebase Timestamp. Unfortunately, java.util.Date is limited to milliseconds, even less granular than the microseconds of the data type within Firestore. Converting to Date means data loss.

Construct a Firestore Timestamp using the other constructor method, where you pass a count of whole seconds plus nanos.

Timestamp ts = new Timestamp( instant.getEpochSecond() , instant.getNano() ) ;

When retrieving a Firestore Timestamp, immediately convert to Instant.

Instant instant = Instant.of( ts.getSeconds() , ts.getNanoseconds() ) ;

If you want to see that same moment adjusted from UTC to some time zone, apply a ZoneId to get a ZonedDateTime.

ZoneId z = ZoneId.of( "Asia/Tokyo" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;  // Same moment, same point on the timeline, different wall-clock time.

Notice that in none of the code above did we work in strings. So no need for any formatting pattern.

Learn to do all your business logic in java.time. Avoid the terrible Date, Calendar, and SimpleDateFormat classes.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
1

Date objects in Java only have a resolution of milliseconds. They can not store any unit of measurement smaller than that. Firestore timestamps can store time units as small as nanoseconds, which is much much smaller than a millisecond.

So, if you convert a Date to a Firestore Timestamp object, the nanoseconds component will always be 0 because there are are simply no nanos available from the Date object. 0 is just the default in this case.

To put it more formally, converting a Timestamp to a Date is narrowing conversion, where you lose precision. Converting a Date to a Timestamp is widening conversion, where new precision is added, and missing data set to zero by default.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441