Repository: pydata/xarray
Issue: Problems parsing time variable using open_dataset

I'm noticing a problem parsing the time variable for at least the `noleap` calendar for a properly formatted time dimension.  Any thoughts on why this is?

``` bash
ncdump -c -t sample_for_xray.nc 
netcdf sample_for_xray {
dimensions:
    time = UNLIMITED ; // (4 currently)
    y = 205 ;
    x = 275 ;
variables:
    double Wind(time, y, x) ;
        Wind:units = "m/s" ;
        Wind:long_name = "Wind speed" ;
        Wind:coordinates = "latitude longitude" ;
        Wind:dimensions = "2" ;
        Wind:type_preferred = "double" ;
        Wind:time_rep = "instantaneous" ;
        Wind:_FillValue = 9.96920996838687e+36 ;
    double time(time) ;
        time:calendar = "noleap" ;
        time:dimensions = "1" ;
        time:long_name = "time" ;
        time:type_preferred = "int" ;
        time:units = "days since 0001-01-01 0:0:0" ;

// global attributes:
        ...
data:

 time = "1979-09-16 12", "1979-10-17", "1979-11-16 12", "1979-12-17" ;
```

``` python
ds = xray.open_dataset('sample_for_xray.nc')
print ds['time']
```

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-46-65c280e7a283> in <module>()
      1 ds = xray.open_dataset('sample_for_xray.nc')
----> 2 print ds['time']

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/common.pyc in __repr__(self)
     40 
     41     def __repr__(self):
---> 42         return array_repr(self)
     43 
     44     def _iter(self):

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/common.pyc in array_repr(arr)
    122     summary = ['<xray.%s %s(%s)>'% (type(arr).__name__, name_str, dim_summary)]
    123     if arr.size < 1e5 or arr._in_memory():
--> 124         summary.append(repr(arr.values))
    125     else:
    126         summary.append('[%s values with dtype=%s]' % (arr.size, arr.dtype))

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/data_array.pyc in values(self)
    147     def values(self):
    148         """The variables's data as a numpy.ndarray"""
--> 149         return self.variable.values
    150 
    151     @values.setter

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/variable.pyc in values(self)
    217     def values(self):
    218         """The variable's data as a numpy.ndarray"""
--> 219         return utils.as_array_or_item(self._data_cached())
    220 
    221     @values.setter

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/utils.pyc in as_array_or_item(values, dtype)
     56         # converted into an integer instead :(
     57         return values
---> 58     values = as_safe_array(values, dtype=dtype)
     59     if values.ndim == 0 and values.dtype.kind == 'O':
     60         # unpack 0d object arrays to be consistent with numpy

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/utils.pyc in as_safe_array(values, dtype)
     40     """Like np.asarray, but convert all datetime64 arrays to ns precision
     41     """
---> 42     values = np.asarray(values, dtype=dtype)
     43     if values.dtype.kind == 'M':
     44         # np.datetime64

/home/jhamman/anaconda/lib/python2.7/site-packages/numpy/core/numeric.pyc in asarray(a, dtype, order)
    458 
    459     """
--> 460     return array(a, dtype, copy=False, order=order)
    461 
    462 def asanyarray(a, dtype=None, order=None):

/home/jhamman/anaconda/lib/python2.7/site-packages/xray/variable.pyc in __array__(self, dtype)
    121         if dtype is None:
    122             dtype = self.dtype
--> 123         return self.array.values.astype(dtype)
    124 
    125     def __getitem__(self, key):

TypeError: Cannot cast datetime.date object from metadata [D] to [ns] according to the rule 'same_kind'
```

This file is available here: ftp://ftp.hydro.washington.edu/pub/jhamman/sample_for_xray.nc


Changed files:
test/test_conventions.py, xray/conventions.py

diff --git a/test/test_conventions.py b/test/test_conventions.py
index 38eb54e15..60c3c9842 100644
--- a/test/test_conventions.py
+++ b/test/test_conventions.py
@@ -110,6 +110,30 @@ class TestDatetime(TestCase):
                         pd.Index(actual), units, calendar)
                     self.assertArrayEqual(num_dates, np.around(encoded))
 
+    @requires_netCDF4
+    def test_decoded_cf_datetime_array(self):
+        import netCDF4 as nc4
+
+        actual = conventions.DecodedCFDatetimeArray(
+            [0, 1, 2], 'days since 1900-01-01', 'standard')
+        expected = pd.date_range('1900-01-01', periods=3).values
+        self.assertEqual(actual.dtype, np.dtype('datetime64[ns]'))
+        self.assertArrayEqual(actual, expected)
+
+        # default calendar
+        actual = conventions.DecodedCFDatetimeArray(
+            [0, 1, 2], 'days since 1900-01-01')
+        self.assertEqual(actual.dtype, np.dtype('datetime64[ns]'))
+        self.assertArrayEqual(actual, expected)
+
+        num_dates = [722000, 720000.5]
+        units = 'days since 0001-01-01 0:0:0'
+        calendar = 'noleap'
+        actual = conventions.DecodedCFDatetimeArray(num_dates, units, calendar)
+        expected = nc4.num2date(num_dates, units, calendar)
+        self.assertEqual(actual.dtype, np.dtype('O'))
+        self.assertArrayEqual(actual, expected)
+
     @requires_netCDF4
     def test_cf_datetime_nan(self):
         for num_dates, units, expected_list in [
diff --git a/xray/conventions.py b/xray/conventions.py
index e8d5e21a6..cecb9025d 100644
--- a/xray/conventions.py
+++ b/xray/conventions.py
@@ -24,6 +24,9 @@ _reserved_names = set(['byte', 'char', 'short', 'ushort', 'int', 'uint',
 # coerced instead as indicated by the "coerce_nc3_dtype" function
 _nc3_dtype_coercions = {'int64': 'int32', 'float64': 'float32', 'bool': 'int8'}
 
+# standard calendars recognized by netcdftime
+_STANDARD_CALENDARS = {'standard', 'gregorian', 'proleptic_gregorian'}
+
 
 def coerce_nc3_dtype(arr):
     """Coerce an array to a data type that can be stored in a netCDF-3 file
@@ -163,7 +166,7 @@ def decode_cf_datetime(num_dates, units, calendar=None):
     else:
         max_date = min_date
 
-    if (calendar not in ['standard', 'gregorian', 'proleptic_gregorian']
+    if (calendar not in _STANDARD_CALENDARS
             or min_date < datetime(1678, 1, 1)
             or max_date > datetime(2262, 4, 11)):
         dates = nc4.num2date(num_dates, units, calendar)
@@ -324,7 +327,13 @@ class DecodedCFDatetimeArray(utils.NDArrayMixin):
 
     @property
     def dtype(self):
-        return np.dtype('datetime64[ns]')
+        if self.calendar is None or self.calendar in _STANDARD_CALENDARS:
+            # TODO: return the proper dtype (object) for a standard calendar
+            # that can't be expressed in ns precision. Perhaps we could guess
+            # this from the units?
+            return np.dtype('datetime64[ns]')
+        else:
+            return np.dtype('O')
 
     def __getitem__(self, key):
         return decode_cf_datetime(self.array, units=self.units,