From cb4b6d4fc5e988629ca0d12fe188c77a2a5544ae Mon Sep 17 00:00:00 2001 From: Jude Kwashie <judekwashie70@gmail.com> Date: Thu, 13 Mar 2025 13:33:53 +0000 Subject: [PATCH 1/4] fix(cloud_firestore): correct nanoseconds calculation for pre-1970 dates --- .../lib/src/timestamp.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart index c29fef9d6e12..b975f4018d69 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart @@ -40,8 +40,12 @@ class Timestamp implements Comparable<Timestamp> { /// Create a [Timestamp] fromMicrosecondsSinceEpoch factory Timestamp.fromMicrosecondsSinceEpoch(int microseconds) { - final int seconds = microseconds ~/ _kMillion; - final int nanoseconds = (microseconds - seconds * _kMillion) * _kThousand; + int seconds = microseconds ~/ _kMillion; + int nanoseconds = (microseconds - seconds * _kMillion) * _kThousand; + if (nanoseconds < 0) { + seconds -= 1; + nanoseconds += _kBillion; + } return Timestamp(seconds, nanoseconds); } From 024e0f498426f1fd654c31510533837a6314ffe3 Mon Sep 17 00:00:00 2001 From: Jude Kwashie <judekwashie70@gmail.com> Date: Thu, 13 Mar 2025 16:07:46 +0000 Subject: [PATCH 2/4] chore: add test to ensure Timestamp.fromDate handles pre-1970 dates correctly --- .../test/timestamp_test.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart index e631213a352e..bfd595095cd2 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart @@ -80,5 +80,24 @@ void main() { expect(epoch, equals(-9999999999)); }); + + test('Timestamp should not throw for dates before 1970', () { + final dates = [ + DateTime(1969, 06, 22, 0, 0, 0, 123), + DateTime(1969, 12, 31, 23, 59, 59, 999), + DateTime(1900, 01, 01, 12, 30, 45, 500), + DateTime(1800, 07, 04, 18, 15, 30, 250), + DateTime(0001, 01, 01, 00, 00, 00, 001), + ]; + + for (final date in dates) { + try { + final timestamp = Timestamp.fromDate(date); + expect(timestamp, isA<Timestamp>()); + } catch (e) { + fail('Timestamp.fromDate threw an error: $e'); + } + } + }); }); } From 6484c796ea69ccdfdb2debf75c531e9a96277cd0 Mon Sep 17 00:00:00 2001 From: Jude Kwashie <judekwashie70@gmail.com> Date: Thu, 13 Mar 2025 16:13:24 +0000 Subject: [PATCH 3/4] chore: add native implementation reference --- .../cloud_firestore_platform_interface/lib/src/timestamp.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart index b975f4018d69..c14faf4dd3ab 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/timestamp.dart @@ -42,6 +42,9 @@ class Timestamp implements Comparable<Timestamp> { factory Timestamp.fromMicrosecondsSinceEpoch(int microseconds) { int seconds = microseconds ~/ _kMillion; int nanoseconds = (microseconds - seconds * _kMillion) * _kThousand; + + // Matches implementation in Android SDK: + // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-common/src/main/java/com/google/firebase/Timestamp.kt#L114-L121 if (nanoseconds < 0) { seconds -= 1; nanoseconds += _kBillion; From d3dca0a3e1c6a9013c4001722eb903de920ddb8e Mon Sep 17 00:00:00 2001 From: Jude Kwashie <judekwashie70@gmail.com> Date: Fri, 14 Mar 2025 12:11:50 +0000 Subject: [PATCH 4/4] chore: add more tests --- .../example/integration_test/timestamp_e2e.dart | 17 +++++++++++++++++ .../test/timestamp_test.dart | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/cloud_firestore/cloud_firestore/example/integration_test/timestamp_e2e.dart b/packages/cloud_firestore/cloud_firestore/example/integration_test/timestamp_e2e.dart index c72564cf5d45..8c78853df295 100644 --- a/packages/cloud_firestore/cloud_firestore/example/integration_test/timestamp_e2e.dart +++ b/packages/cloud_firestore/cloud_firestore/example/integration_test/timestamp_e2e.dart @@ -53,5 +53,22 @@ void runTimestampTests() { equals(date.millisecondsSinceEpoch), ); }); + + test('set pre-1970 $Timestamp and return', () async { + DocumentReference<Map<String, dynamic>> doc = + await initializeTest('timestamp'); + final date = DateTime(1969, 06, 22, 0, 0, 0, 123); + final localTimestamp = Timestamp.fromDate(date); + + await doc.set({'foo': localTimestamp}); + + DocumentSnapshot<Map<String, dynamic>> snapshot = await doc.get(); + Timestamp retievedTimestamp = snapshot.data()!['foo']; + expect(retievedTimestamp, isA<Timestamp>()); + expect( + retievedTimestamp, + equals(localTimestamp), + ); + }); }); } diff --git a/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart b/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart index bfd595095cd2..232d48bb8900 100644 --- a/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart +++ b/packages/cloud_firestore/cloud_firestore_platform_interface/test/timestamp_test.dart @@ -99,5 +99,15 @@ void main() { } } }); + + test( + 'pre-1970 Timestamps should match the original DateTime after conversion', + () { + final date = DateTime(1969, 06, 22, 0, 0, 0, 123); + final timestamp = Timestamp.fromDate(date); + final timestampAsDateTime = timestamp.toDate(); + + expect(date, equals(timestampAsDateTime)); + }); }); }