Repository: opendatacube/datacube-core

Issue:
Allow open-ended time ranges
When specifying a time range, be able to have the start or end of the range be open, to indicate all data before or after a date. For example:

`time=('2001', None)`
could indicate all data from 2001 onward.

`time=(None, '2010')`
could indicate all data before 2010.



Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Resolving patch:
diff --git a/datacube/api/core.py b/datacube/api/core.py
--- a/datacube/api/core.py
+++ b/datacube/api/core.py
@@ -232,12 +232,17 @@ def load(self, product=None, measurements=None, output_crs=None, resolution=None
 
                 x=(1516200, 1541300), y=(-3867375, -3867350), crs='EPSG:3577'
 
-            The ``time`` dimension can be specified using a tuple of datetime objects or strings with
-            ``YYYY-MM-DD hh:mm:ss`` format. Data will be loaded inclusive of the start and finish times. E.g::
+            The ``time`` dimension can be specified using a single or tuple of datetime objects or strings with
+            ``YYYY-MM-DD hh:mm:ss`` format. Data will be loaded inclusive of the start and finish times.
+            A ``None`` value in the range indicates an open range, with the provided date serving as either the
+            upper or lower bound. E.g::
 
                 time=('2000-01-01', '2001-12-31')
                 time=('2000-01', '2001-12')
                 time=('2000', '2001')
+                time=('2000')
+                time=('2000', None)  # all data from 2000 onward
+                time=(None, '2000')  # all data up to and including 2000
 
             For 3D datasets, where the product definition contains an ``extra_dimension`` specification,
             these dimensions can be queried using that dimension's name. E.g.::
diff --git a/datacube/api/query.py b/datacube/api/query.py
--- a/datacube/api/query.py
+++ b/datacube/api/query.py
@@ -128,8 +128,8 @@ def __init__(self, index=None, product=None, geopolygon=None, like=None, **searc
                 if time_coord is not None:
                     self.search['time'] = _time_to_search_dims(
                         (pandas_to_datetime(time_coord.values[0]).to_pydatetime(),
-                         pandas_to_datetime(time_coord.values[-1]).to_pydatetime()
-                         + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches
+                            pandas_to_datetime(time_coord.values[-1]).to_pydatetime()
+                            + datetime.timedelta(milliseconds=1))  # TODO: inclusive time searches
                     )
 
     @property
@@ -342,7 +342,11 @@ def _time_to_search_dims(time_range):
         if hasattr(tr_end, 'isoformat'):
             tr_end = tr_end.isoformat()
 
+        if tr_start is None:
+            tr_start = datetime.datetime.fromtimestamp(0)
         start = _to_datetime(tr_start)
+        if tr_end is None:
+            tr_end = datetime.datetime.now().strftime("%Y-%m-%d")
         end = _to_datetime(pandas.Period(tr_end)
                            .end_time
                            .to_pydatetime())
@@ -354,19 +358,6 @@ def _time_to_search_dims(time_range):
         return tr
 
 
-def _time_to_open_range(time, lower_bound: bool):
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore", UserWarning)
-
-        if lower_bound:  # from date provided (not inclusive) to latest available
-            start = _to_datetime(pandas.Period(time).end_time.to_pydatetime())
-            end = _to_datetime(datetime.datetime.now())
-        else:  # from earliest available to date provided (not inclusive)
-            start = _to_datetime(datetime.datetime.fromtimestamp(0))
-            end = _to_datetime(pandas.Period(time).start_time.to_pydatetime())
-        return Range(start, end)
-
-
 def _convert_to_solar_time(utc, longitude):
     seconds_per_degree = 240
     offset_seconds = int(longitude * seconds_per_degree)
diff --git a/datacube/ui/expression.py b/datacube/ui/expression.py
--- a/datacube/ui/expression.py
+++ b/datacube/ui/expression.py
@@ -20,7 +20,7 @@
 
 from lark import Lark, v_args, Transformer
 
-from datacube.api.query import _time_to_search_dims, _time_to_open_range
+from datacube.api.query import _time_to_search_dims
 from datacube.model import Range
 
 
@@ -119,11 +119,10 @@ def date_pair(self, start, end):
         return _time_to_search_dims((start, end))
     
     def range_lower_bound(self, date):
-        return _time_to_open_range(date, lower_bound=True)
+        return _time_to_search_dims((date, None))
 
     def range_upper_bound(self, date):
-        return _time_to_open_range(date, lower_bound=False)
-
+        return _time_to_search_dims((None, date))
 
     def date(self, y, m=None, d=None):
         return "-".join(x for x in [y, m, d] if x is not None)


Test patch:
diff --git a/tests/api/test_query.py b/tests/api/test_query.py
--- a/tests/api/test_query.py
+++ b/tests/api/test_query.py
@@ -140,7 +140,11 @@ def format_test(start_out, end_out):
     ((datetime.datetime(2008, 1, 1), datetime.datetime(2008, 1, 10, 23, 59, 40)),
      format_test('2008-01-01T00:00:00', '2008-01-10T23:59:40.999999')),
     ((datetime.date(2008, 1, 1)),
-     format_test('2008-01-01T00:00:00', '2008-01-01T23:59:59.999999'))
+     format_test('2008-01-01T00:00:00', '2008-01-01T23:59:59.999999')),
+    ((datetime.date(2008, 1, 1), None),
+     format_test('2008-01-01T00:00:00', datetime.datetime.now().strftime("%Y-%m-%dT23:59:59.999999"))),
+    ((None, '2008'),
+     format_test(datetime.datetime.fromtimestamp(0).strftime("%Y-%m-%dT%H:%M:%S"), '2008-12-31T23:59:59.999999'))
 ]