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
Dateto UTC) - When you read Date/Timestamp from Firestore, I believe Firestore auto convert from UTC to Local Timezone.
- I personally prefer to use
Timestampcompared toDateto 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
Timestampmethod to work with Java Date:Date toDate()andTimestamp(Date date). - There is no convenient
Timestampmethod 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 Timestampval seconds = localDateTime.atZone(tz).toEpochSecond()val nanos = localDateTime.nanoval timestamp = com.google.firebase.Timestamp(seconds, nanos)You can create Date directly.
val date = Date()val date = Calendar.getInstance().timeYou 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.147date_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 maintaindb.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.147NOTE: 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 2and excute the code. - Set Android Devie Timezone to Bangkok (GMT+9), change
"sequence" to 3and excute the code. - Set Android Devie Timezone to Kuala Lumpur (GMT+8), change
"sequence" to 4and 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=4Sorting 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.