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 toDate
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()
andTimestamp(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 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().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 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.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.