Skip to content

Commit 91afd54

Browse files
committed
BUG: minimal out of bounds checking on future/past timestamps, gotcha docs, pre-1900 repr
1 parent 7240b87 commit 91afd54

File tree

3 files changed

+74
-5
lines changed

3 files changed

+74
-5
lines changed

doc/source/gotchas.rst

+24
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,27 @@ passed in the index, thus finding the integers ``0`` and ``1``. While it would
217217
be possible to insert some logic to check whether a passed sequence is all
218218
contained in the index, that logic would exact a very high cost in large data
219219
sets.
220+
221+
Timestamp limitations
222+
---------------------
223+
224+
Minimum and maximum timestamps
225+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
226+
227+
Since pandas represents timestamps in nanosecond resolution, the timespan that
228+
can be represented using a 64-bit integer is limited to approximately 584 years:
229+
230+
.. ipython:: python
231+
232+
begin = Timestamp(-9223285636854775809L)
233+
begin
234+
end = Timestamp(np.iinfo(np.int64).max)
235+
end
236+
237+
If you need to represent time series data outside the nanosecond timespan, use
238+
PeriodIndex:
239+
240+
.. ipython:: python
241+
242+
span = period_range('1215-01-01', '1381-01-01', freq='D')
243+
span

pandas/src/datetime.pyx

+36-4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def ints_to_pydatetime(ndarray[int64_t] arr, tz=None):
6565
return result
6666

6767

68+
6869
# Python front end to C extension type _Timestamp
6970
# This serves as the box for datetime64
7071
class Timestamp(_Timestamp):
@@ -101,10 +102,27 @@ class Timestamp(_Timestamp):
101102
return ts_base
102103

103104
def __repr__(self):
104-
result = self.strftime('<Timestamp: %Y-%m-%d %H:%M:%S%z')
105-
if self.tzinfo:
106-
result += self.strftime(' %%Z, tz=%s' % self.tzinfo.zone)
107-
return result + '>'
105+
result = '%d-%.2d-%.2d %.2d:%.2d:%.2d' % (self.year, self.month,
106+
self.day, self.hour,
107+
self.minute, self.second)
108+
109+
if self.nanosecond != 0:
110+
nanos = self.nanosecond + 1000 * self.microsecond
111+
result += '.%.9d' % nanos
112+
elif self.microsecond != 0:
113+
result += '.%.6d' % self.microsecond
114+
115+
try:
116+
result += self.strftime('%z')
117+
if self.tzinfo:
118+
result += self.strftime(' %%Z, tz=%s' % self.tzinfo.zone)
119+
except ValueError:
120+
year2000 = self.replace(year=2000)
121+
result += year2000.strftime('%z')
122+
if self.tzinfo:
123+
result += year2000.strftime(' %%Z, tz=%s' % self.tzinfo.zone)
124+
125+
return '<Timestamp: %s>' % result
108126

109127
@property
110128
def tz(self):
@@ -507,13 +525,17 @@ cpdef convert_to_tsobject(object ts, object tz=None):
507525
obj.tzinfo = ts.tzinfo
508526
if obj.tzinfo is not None:
509527
obj.value -= _delta_to_nanoseconds(obj.tzinfo._utcoffset)
528+
_check_dts_bounds(obj.value, &obj.dts)
510529
return obj
511530
elif PyDate_Check(ts):
512531
obj.value = _date_to_datetime64(ts, &obj.dts)
513532
else:
514533
raise ValueError("Could not construct Timestamp from argument %s" %
515534
type(ts))
516535

536+
if obj.value != NPY_NAT:
537+
_check_dts_bounds(obj.value, &obj.dts)
538+
517539
if tz is not None:
518540
if tz is pytz.utc:
519541
obj.tzinfo = tz
@@ -530,6 +552,16 @@ cpdef convert_to_tsobject(object ts, object tz=None):
530552

531553
return obj
532554

555+
cdef int64_t _NS_LOWER_BOUND = -9223285636854775809LL
556+
cdef int64_t _NS_UPPER_BOUND = -9223372036854775807LL
557+
558+
cdef inline _check_dts_bounds(int64_t value, pandas_datetimestruct *dts):
559+
cdef pandas_datetimestruct dts2
560+
if dts.year <= 1677 or dts.year >= 2262:
561+
pandas_datetime_to_datetimestruct(value, PANDAS_FR_ns, &dts2)
562+
if dts2.year != dts.year:
563+
raise ValueError('Out of bounds timestamp in year: %s' % dts.year)
564+
533565
# elif isinstance(ts, _Timestamp):
534566
# tmp = ts
535567
# obj.value = (<_Timestamp> ts).value

pandas/tseries/tests/test_timeseries.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,20 @@ def test_timestamp_fields(self):
898898
self.assertEqual(idx.freq, Timestamp(idx[0], idx.freq).freq)
899899
self.assertEqual(idx.freqstr, Timestamp(idx[0], idx.freq).freqstr)
900900

901+
def test_timestamp_date_out_of_range(self):
902+
self.assertRaises(ValueError, Timestamp, '1676-01-01')
903+
self.assertRaises(ValueError, Timestamp, '2263-01-01')
904+
905+
def test_timestamp_repr(self):
906+
# pre-1900
907+
stamp = Timestamp('1850-01-01', tz='US/Eastern')
908+
repr(stamp)
909+
910+
iso8601 = '1850-01-01 01:23:45.012345'
911+
stamp = Timestamp(iso8601, tz='US/Eastern')
912+
result = repr(stamp)
913+
self.assert_(iso8601 in result)
914+
901915
def test_datetimeindex_integers_shift(self):
902916
rng = date_range('1/1/2000', periods=20)
903917

@@ -918,7 +932,6 @@ def test_astype_object(self):
918932

919933
self.assert_(np.array_equal(casted, exp_values))
920934

921-
922935
def test_catch_infinite_loop(self):
923936
offset = datetools.DateOffset(minute=5)
924937
# blow up, don't loop forever

0 commit comments

Comments
 (0)