Skip to content

Commit

Permalink
Add support for negative timestamps (#1060)
Browse files Browse the repository at this point in the history
  • Loading branch information
gutsytechster committed Jun 17, 2022
1 parent 89b7fda commit a7ca7a5
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -1,4 +1,5 @@
*.py[cod]
.python-version

# C extensions
*.so
Expand Down Expand Up @@ -47,6 +48,7 @@ docs/_build
# Editors
*.swp
.idea
.vscode/

# Other
raw_data
2 changes: 1 addition & 1 deletion dateparser/conf.py
Expand Up @@ -120,7 +120,7 @@ def _check_require_part(setting_name, setting_value):
def _check_parsers(setting_name, setting_value):
"""Returns `True` if the provided list of parsers contains valid values"""
existing_parsers = [
'timestamp', 'relative-time', 'custom-formats', 'absolute-time', 'no-spaces-time'
'timestamp', 'relative-time', 'custom-formats', 'absolute-time', 'no-spaces-time', 'negative-timestamp'
] # FIXME: Extract the list of existing parsers from another place (#798)
unknown_parsers = set(setting_value) - set(existing_parsers)
if unknown_parsers:
Expand Down
20 changes: 16 additions & 4 deletions dateparser/date.py
Expand Up @@ -40,6 +40,7 @@
RE_SANITIZE_APOSTROPHE = re.compile('|'.join(APOSTROPHE_LOOK_ALIKE_CHARS))

RE_SEARCH_TIMESTAMP = re.compile(r'^(\d{10})(\d{3})?(\d{3})?(?![^.])')
RE_SEARCH_NEGATIVE_TIMESTAMP = re.compile(r'^([-]\d{10})(\d{3})?(\d{3})?(?![^.])')


def sanitize_spaces(date_string):
Expand Down Expand Up @@ -112,8 +113,12 @@ def sanitize_date(date_string):
return date_string


def get_date_from_timestamp(date_string, settings):
match = RE_SEARCH_TIMESTAMP.search(date_string)
def get_date_from_timestamp(date_string, settings, negative=False):
if negative:
match = RE_SEARCH_NEGATIVE_TIMESTAMP.search(date_string)
else:
match = RE_SEARCH_TIMESTAMP.search(date_string)

if match:
seconds = int(match.group(1))
millis = int(match.group(2) or 0)
Expand Down Expand Up @@ -166,6 +171,7 @@ def __init__(self, locale, date_string, date_formats, settings=None):
self._translated_date_with_formatting = None
self._parsers = {
'timestamp': self._try_timestamp,
'negative-timestamp': self._try_negative_timestamp,
'relative-time': self._try_freshness_parser,
'custom-formats': self._try_given_formats,
'absolute-time': self._try_absolute_parser,
Expand All @@ -185,12 +191,18 @@ def _parse(self):
else:
return None

def _try_timestamp(self):
def _try_timestamp_parser(self, negative=False):
return DateData(
date_obj=get_date_from_timestamp(self.date_string, self._settings),
date_obj=get_date_from_timestamp(self.date_string, self._settings, negative=negative),
period='time' if self._settings.RETURN_TIME_AS_PERIOD else 'day',
)

def _try_timestamp(self):
return self._try_timestamp_parser()

def _try_negative_timestamp(self):
return self._try_timestamp_parser(negative=True)

def _try_freshness_parser(self):
try:
return freshness_date_parser.get_date_data(self._get_translated_date(), self._settings)
Expand Down
5 changes: 4 additions & 1 deletion docs/settings.rst
Expand Up @@ -144,7 +144,7 @@ Language Detection
Default Languages
+++++++++++++++++

``DEFAULT_LANGUAGES``: It is a ``list`` of language codes in ISO 639 that will be used as default
``DEFAULT_LANGUAGES``: It is a ``list`` of language codes in ISO 639 that will be used as default
languages for parsing when language detection fails. eg. ["en", "fr"]:

>>> from dateparser import parse
Expand Down Expand Up @@ -181,6 +181,9 @@ The following parsers exist:
followed by additional digits or a period (``.``), those first 10 digits
are interpreted as `Unix time <https://en.wikipedia.org/wiki/Unix_time>`_.

- ``'negative-timestamp'``: ``'timestamp'`` for negative timestamps. For
example, parses ``-186454800000`` as ``1964-02-03T23:00:00``.

- ``'relative-time'``: Parses dates and times expressed in relation to the
current date and time (e.g. “1 day ago”, “in 2 weeks”).

Expand Down
28 changes: 28 additions & 0 deletions tests/test_date.py
Expand Up @@ -726,6 +726,34 @@ def test_timestamp_in_microseconds(self):
datetime.fromtimestamp(1570308760).replace(microsecond=263111)
)

@parameterized.expand([
param(
input_timestamp='-1570308760',
negative=True,
result=datetime.fromtimestamp(-1570308760)
),
param(
input_timestamp='-1570308760',
negative=False,
result=None
),
param(
input_timestamp='1570308760',
negative=True,
result=None
),
param(
input_timestamp='1570308760',
negative=False,
result=datetime.fromtimestamp(1570308760)
)
])
def test_timestamp_with_negative(self, input_timestamp, negative, result):
self.assertEqual(
date.get_date_from_timestamp(input_timestamp, None, negative=negative),
result
)

@parameterized.expand([
param(date_string='15703087602631'),
param(date_string='157030876026xx'),
Expand Down
12 changes: 12 additions & 0 deletions tests/test_date_parser.py
Expand Up @@ -645,6 +645,18 @@ def test_parse_timestamp(self, date_string, expected):
self.then_date_obj_exactly_is(expected)
self.then_period_is('day')

@parameterized.expand([
param('-1484823450', expected=datetime(1922, 12, 13, 13, 2, 30)),
param('-1436745600000', expected=datetime(1924, 6, 22, 0, 0)),
param('-1015673450000001', expected=datetime(1937, 10, 25, 12, 29, 10, 1))
])
def test_parse_negative_timestamp(self, date_string, expected):
self.given_local_tz_offset(0)
self.given_parser(settings={'TO_TIMEZONE': 'UTC', 'PARSERS': ['negative-timestamp']})
self.when_date_is_parsed(date_string)
self.then_date_obj_exactly_is(expected)
self.then_period_is('day')

@parameterized.expand([
# Epoch timestamps.
param('1484823450', expected=datetime(2017, 1, 19, 10, 57, 30)),
Expand Down

0 comments on commit a7ca7a5

Please sign in to comment.