Repository: voxel51/fiftyone
Issue: [BUG] Filtering by a datetime field near the epoch raises an exception

### System information

-   **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Ubuntu 20.04.2 LTS
-   **FiftyOne installed from (pip or source)**: pip
-   **FiftyOne version (run `fiftyone --version`)**: v0.14.2
-   **Python version**: 3.8

### Commands to reproduce

-

### Describe the problem

When trying to filter by the sliders on a datetime field in the app, the server raises an uncaught exception, hence the filtering is not done.

### Code to reproduce issue

-

### Other info / logs

```
An exception has been raised by the server. Your session has been reverted to its previous state.

Traceback (most recent call last):
  File "main.py", line 313, in wrapper
    result = await func(self, *args, **kwargs)
  File "main.py", line 556, in on_message
    await event(self, **message)
  File "main.py", line 624, in on_filters_update
    await self.send_statistics(view, filters=filters, extended=True)
  File "main.py", line 1031, in send_statistics
    results = await view._async_aggregate(aggregations)
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/fiftyone/core/collections.py", line 6784, in _async_aggregate
    _results = await foo.aggregate(collection, pipelines)
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/fiftyone/core/odm/database.py", line 191, in _do_async_pooled_aggregate
    return await asyncio.gather(
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/fiftyone/core/odm/database.py", line 201, in _do_async_aggregate
    return await cursor.to_list(1)
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/motor/core.py", line 1417, in _to_list
    result = get_more_result.result()
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/motor/core.py", line 1626, in _on_started
    pymongo_cursor = future.result()
  File "/usr/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/collection.py", line 2502, in aggregate
    return self._aggregate(_CollectionAggregationCommand,
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/collection.py", line 2419, in _aggregate
    return self.__database.client._retryable_read(
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/mongo_client.py", line 1525, in _retryable_read
    return func(session, server, sock_info, secondary_ok)
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/aggregation.py", line 137, in get_cursor
    result = sock_info.command(
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/pool.py", line 710, in command
    return command(self, dbname, spec, secondary_ok,
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/network.py", line 161, in command
    helpers._check_command_response(
  File "/home/fhe2bp/venvs/datalake/lib/python3.8/site-packages/pymongo/helpers.py", line 167, in _check_command_response
    raise OperationFailure(errmsg, code, response, max_wire_version)
pymongo.errors.OperationFailure: Failed to optimize pipeline :: caused by :: Failed to optimize expression :: caused by :: Unsupported conversion from int to date in $convert with no onError value, full error: {'ok': 0.0, 'errmsg': 'Failed to optimize pipeline :: caused by :: Failed to optimize expression :: caused by :: Unsupported conversion from int to date in $convert with no onError value', 'code': 241, 'codeName': 'ConversionFailure'}
```

### What areas of FiftyOne does this bug affect?

-   [ ] `App`: FiftyOne application issue
-   [ ] `Core`: Core `fiftyone` Python library issue
-   [x] `Server`: Fiftyone server issue

### Willingness to contribute

The FiftyOne Community encourages bug fix contributions. Would you or another
member of your organization be willing to contribute a fix for this bug to the
FiftyOne codebase?

-   [ ] Yes. I can contribute a fix for this bug independently.
-   [x] Yes. I would be willing to contribute a fix for this bug with guidance
        from the FiftyOne community.
-   [ ] No. I cannot contribute a bug fix at this time.


Changed files:
fiftyone/core/expressions.py, tests/unittests/view_tests.py, fiftyone/core/utils.py

diff --git a/fiftyone/core/expressions.py b/fiftyone/core/expressions.py
index 482239209..e0f03f134 100644
--- a/fiftyone/core/expressions.py
+++ b/fiftyone/core/expressions.py
@@ -4616,6 +4616,7 @@ def _do_to_mongo(val, prefix):
         return [_do_to_mongo(v, prefix) for v in val]
 
     if isinstance(val, (date, datetime)):
+        # The arg needs must be float (not int) to avoid errors near the epoch
         return {"$toDate": fou.datetime_to_timestamp(val)}
 
     if isinstance(val, timedelta):
diff --git a/fiftyone/core/utils.py b/fiftyone/core/utils.py
index 2e0e7cb42..f2376e751 100644
--- a/fiftyone/core/utils.py
+++ b/fiftyone/core/utils.py
@@ -1246,7 +1246,7 @@ def datetime_to_timestamp(dt):
         dt: a `datetime.date` or `datetime.datetime`
 
     Returns:
-        the number of milliseconds since epoch
+        the float number of milliseconds since epoch
     """
     if type(dt) is date:
         dt = datetime(dt.year, dt.month, dt.day)
@@ -1254,7 +1254,7 @@ def datetime_to_timestamp(dt):
     if dt.tzinfo is None:
         dt = dt.replace(tzinfo=pytz.utc)
 
-    return int(1000 * dt.timestamp())
+    return 1000.0 * dt.timestamp()
 
 
 def timestamp_to_datetime(ts):
@@ -1267,7 +1267,7 @@ def timestamp_to_datetime(ts):
     Returns:
         a `datetime.datetime`
     """
-    dt = datetime.utcfromtimestamp(ts / 1000)
+    dt = datetime.utcfromtimestamp(ts / 1000.0)
 
     if fo.config.timezone is None:
         return dt
@@ -1283,10 +1283,10 @@ def timedelta_to_ms(td):
         td: a `datetime.timedelta`
 
     Returns:
-        the number of milliseconds
+        the float number of milliseconds
     """
-    return int(
-        86400000 * td.days + 1000 * td.seconds + td.microseconds // 1000
+    return (
+        86400000.0 * td.days + 1000.0 * td.seconds + td.microseconds / 1000.0
     )
 
 
diff --git a/tests/unittests/view_tests.py b/tests/unittests/view_tests.py
index e1ec282a7..115087ce2 100644
--- a/tests/unittests/view_tests.py
+++ b/tests/unittests/view_tests.py
@@ -589,11 +589,11 @@ class ViewExpressionTests(unittest.TestCase):
     def test_dates(self):
         dataset = fo.Dataset()
 
-        date1 = date(2021, 8, 24)
-        date2 = date(2021, 8, 25)
-        date3 = date(2021, 8, 26)
+        date1 = date(1970, 1, 1)
+        date2 = date(1970, 1, 2)
+        date3 = date(1970, 1, 3)
 
-        query_date = datetime(2021, 8, 25, 1, 0, 0)
+        query_date = datetime(1970, 1, 2, 1, 0, 0)
         query_delta = timedelta(hours=2)
 
         dataset.add_samples(
@@ -626,11 +626,11 @@ class ViewExpressionTests(unittest.TestCase):
     def test_datetimes(self):
         dataset = fo.Dataset()
 
-        date1 = datetime(2021, 8, 24, 1, 0, 0)
-        date2 = datetime(2021, 8, 24, 2, 0, 0)
-        date3 = datetime(2021, 8, 24, 3, 0, 0)
+        date1 = datetime(1970, 1, 1, 1, 0, 0)
+        date2 = datetime(1970, 1, 1, 2, 0, 0)
+        date3 = datetime(1970, 1, 1, 3, 0, 0)
 
-        query_date = datetime(2021, 8, 24, 2, 1, 0)
+        query_date = datetime(1970, 1, 1, 2, 1, 0)
         query_delta = timedelta(minutes=30)
 
         dataset.add_samples(