Understanding Date/Timestamp in Firestore for Multiple Timezones Support

February 28, 2019
Saving DateTime for Multiple Timezones, and using LocalDateTime (java.time) or ThreeTenABP

NOTE: Tested using Android client.

TL;DR

  • On the client (Android), you should save Date/Timestamp in local timezone (use Timestamp.now(), don’t need to get UTC time).
  • I believe Firestore store Timestamp in UTC (auto convert Date to UTC)
  • When you read Date/Timestamp from Firestore, I believe Firestore auto convert from UTC to Local Timezone.
  • I personally prefer to use Timestamp compared to Date to avoid timezone confusion.

About Firestore Date/Timestamp

  • Firestore use Timestamp class to represent DateTime on the client (Android), and it does not store TimeZone information.
  • There is convenient Timestamp method to work with Java Date: Date toDate() and Timestamp(Date date).
  • There is no convenient Timestamp method to work with Java Time or threetenbp or ThreeTenABP.
  • When stored in Cloud Firestore, precise only to microseconds; any additional precision is rounded down - Firebase Supported data types
  • When you view data using Firebase console, the Date/Timestamp is converted to your browser’s local timezone (e.g. 28 February 2019 at 15:53:59 UTC+8)

How to store Date/Timestamp

You can use Java Date or Firebase Timestamp.

I like to use Java Time or ThreeTenABP, so I shall use LocalDateTime to create a date (then convert them to Date or Timestamp).

val tz = ZoneId.systemDefault()
val localDateTime = LocalDateTime.now()

// convert LocalDateTime to Date
// val date = Date.from(localDateTime.atZone(tz).toInstant())
val date = DateTimeUtils.toDate(localDateTime.atZone(tz).toInstant())

// convert LocalDateTime to Timestamp
val seconds = localDateTime.atZone(tz).toEpochSecond()
val nanos = localDateTime.nano
val timestamp = com.google.firebase.Timestamp(seconds, nanos)

You can create Date directly.

val date = Date()
val date = Calendar.getInstance().time

You can create Timestamp directly.

val timestamp = Timestamp.now()

Save Date and Timestamp to Firestore.

You can also use FieldValue.serverTimestamp().

Returns a sentinel for use with set() or update() to include a server-generated timestamp in the written data.

val db = FirebaseFirestore.getInstance()

db.collection("test")
    .document("test_date")
    .set(mapOf(
        "date_firebase_server_timestamp" to FieldValue.serverTimestamp(),
        "date_310bp" to localDateTime, // shouldn't do this
        "date_java_date" to date,
        "date_firebase_timestamp" to timestamp
        ))

How to read Date/Timestamp

NOTE: You can also view data from Firebase Console, but it won’t show the microseconds for Date/Timestamp.

db.collection("test")
    .document("test_date")
    .get()
    .addOnSuccessListener {
        for ((key, value) in it.data!!) {
            Timber.d("key=$key, value=$value")
        }

        Timber.d("---")

        Timber.d("date_firebase_server_timestamp=${it.getDate("date_firebase_server_timestamp")}") 
        Timber.d("date_java_date=${it.getDate("date_java_date")}")      
        Timber.d("date_firebase_timestamp=${it.getDate("date_firebase_timestamp")}")

        # convert Timestamp to LocalDateTime
        val timestamp = it["date_firebase_timestamp"] as com.google.firebase.Timestamp
        val milliseconds = timestamp.seconds * 1000 + timestamp.nanoseconds / 1000000
        val tz = ZoneId.systemDefault()
        val localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(milliseconds), tz)
        Timber.d("localDateTime=$localDateTime")        
    }

Output

key=date_firebase_server_timestamp, value=Timestamp(seconds=1551340439, nanoseconds=84000000)

key=date_310bp, value={chronology={id=ISO, calendarType=iso8601}, month=FEBRUARY, hour=15, dayOfYear=59, nano=147000000, minute=53, dayOfWeek=THURSDAY, monthValue=2, dayOfMonth=28, year=2019, second=49}

key=date_java_date, value=Timestamp(seconds=1551340429, nanoseconds=147000000)
key=date_firebase_timestamp, value=Timestamp(seconds=1551340429, nanoseconds=147000000)

---

date_firebase_server_timestamp=Thu Feb 28 15:53:59 GMT+08:00 2019
date_java_date=Thu Feb 28 15:53:49 GMT+08:00 2019
date_firebase_timestamp=Thu Feb 28 15:53:49 GMT+08:00 2019
localDateTime=2019-02-28T15:53:49.147

date_firebase_server_timestamp (server date) will be slightly later (about 10s) than date_java_date and date_firebase_timestamp (local client date).

NOTE: Since the shown server and client date is almost similar, it proves that we have done UTC date storage right by using local TimeZone.

date_310bp using ThreeTenABP using LocalDateTime end up storing class variables, which is not useful or reliable.

NOTE: I don’t think Firestore support a custom coverter/adapter like Moshi or Gson.

Value for date_java_date and date_firebase_timestamp is the same, which is expected.

getDate will return Date with local TimeZone, or you can use Timestamp to construct LocalDateTime.

Prove TimeZone Conversion

To prove that Firestore actually store Date/Timestamp as UTC and convert to the local time zone of the client during retrieval:

  • Change Android device timezone to somewhere else (make sure it is different from your Desktop)
  • Remove/comment the db.set (add/update) code but maintain db.get (read/retrieval) code
  • Force close existing Android application (to make sure the new TimeZome take effect)
  • Run the Android code for data read/retrieval

You shall notice the result on your Android device differ from Firebase Console result shown on your desktop.

I changed Android device to Tokyo (GMT+9) while my Desktop is Kuala Lumpur (GMT+8)

Also, the result on Android device (Tokyo GMT+9) also differ from previous Android device (Kuala Lumpur GMT+8).

The following is result of Android device (Tokyo GMT+9)

date_firebase_server_timestamp=Thu Feb 28 16:53:59 GMT+08:00 2019
date_java_date=Thu Feb 28 16:53:49 GMT+08:00 2019
date_firebase_timestamp=Thu Feb 28 16:53:49 GMT+08:00 2019
localDateTime=2019-02-28T16:53:49.147

NOTE: Compare result of Android device (Tokyo GMT+9) vs the result at the top Android device (Kuala Lumpur GMT+8), and note that they differ by one hour as expected.

Kotlin Extension: Convert LocalDateTime to Timestamp

fun LocalDateTime.toTimestamp() = Timestamp(atZone(ZoneId.systemDefault()).toEpochSecond(), nano)

And convert Timestamp to LocalDateTime.

fun Timestamp.toLocalDateTime(zone: ZoneId = ZoneId.systemDefault()) = LocalDateTime.ofInstant(Instant.ofEpochMilli(seconds * 1000 + nanoseconds / 1000000), zone)

Save Date in Multiple Timezones

Methods:

  • Set Android Devie Timezone to Kuala Lumpur (GMT+8) and execute the following code.
  • Set Android Devie Timezone to Tokyo (GMT+9), change "sequence" to 2 and excute the code.
  • Set Android Devie Timezone to Bangkok (GMT+9), change "sequence" to 3 and excute the code.
  • Set Android Devie Timezone to Kuala Lumpur (GMT+8), change "sequence" to 4 and excute the code.
val db = FirebaseFirestore.getInstance()

db.collection("test").
    add(mapOf(
        "created" to FieldValue.serverTimestamp(),
        "posted_date" to LocalDateTime.now().toTimestamp(),
        "posted_timezone" to ZoneId.systemDefault().id,
        "test_sequence" to true,
        "sequence" to 1
        ))

Perform the following query.

db.collection("test")
    .orderBy("posted_date")
    .whereEqualTo("test_sequence", true)
    .get()
    .addOnSuccessListener { documents ->
        for (document in documents) {
            val postedDate = (document["posted_date"] as Timestamp).toLocalDateTime()
            val localPostedDate = (document["posted_date"] as Timestamp).toLocalDateTime(ZoneId.of(document["posted_timezone"] as String))
            Timber.d("posted_date=$postedDate, local_date=$localPostedDate, timezone=${document["posted_timezone"]}, sequence=${document["sequence"]}")
        }
    }

Output for Kuala Lumpur (GMT+8)

posted_date=2019-02-28T23:38:16.006, local_date=2019-02-28T23:38:16.006, timezone=Asia/Kuala_Lumpur, sequence=1
posted_date=2019-02-28T23:39:38.850, local_date=2019-03-01T00:39:38.850, timezone=Asia/Tokyo, sequence=2
posted_date=2019-02-28T23:41:05.947, local_date=2019-02-28T22:41:05.947, timezone=Asia/Bangkok, sequence=3
posted_date=2019-02-28T23:41:52.449, local_date=2019-02-28T23:41:52.449, timezone=Asia/Kuala_Lumpur, sequence=4

Sorting by posted_date works: which means data entry order is maintained even though timezone changes.

By storing timezone id, we can use it to convert to local date (the actual date/time for the timezone).

NOTE: Read about time zone best practices, saving datetime & timezone info in database and timezone wiki.

NOTE: For some cases, you might even want to store the both utc date and local date into the database.

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.