Skip to content

Commit

Permalink
Merge branch 'master' of github.com:crsmithdev/arrow
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Elkins committed Nov 27, 2016
2 parents 07d6308 + e5fa365 commit 5f235bc
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 60 deletions.
8 changes: 7 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[run]
branch = True
source = arrow
source =
tests
arrow

[report]
show_missing = True
fail_under = 100
2 changes: 1 addition & 1 deletion arrow/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ def factory(type):
return ArrowFactory(type)


__all__ = ['get', 'utcnow', 'now', 'factory', 'iso']
__all__ = ['get', 'utcnow', 'now', 'factory']

3 changes: 1 addition & 2 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ def humanize(self, other=None, locale='en_us', only_distance=False):
Defaults to now in the current :class:`Arrow <arrow.arrow.Arrow>` object's timezone.
:param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'.
:param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part.
Usage::
>>> earlier = arrow.utcnow().replace(hours=-2)
Expand Down Expand Up @@ -753,8 +754,6 @@ def __eq__(self, other):
if not isinstance(other, (Arrow, datetime)):
return False

other = self._get_datetime(other)

return self._datetime == self._get_datetime(other)

def __ne__(self, other):
Expand Down
4 changes: 2 additions & 2 deletions arrow/locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@


def get_locale(name):
'''Returns an appropriate :class:`Locale <locale.Locale>` corresponding
to an inpute locale name.
'''Returns an appropriate :class:`Locale <arrow.locales.Locale>`
corresponding to an inpute locale name.
:param name: the name of the locale.
Expand Down
49 changes: 23 additions & 26 deletions arrow/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@ class ParserError(RuntimeError):

class DateTimeParser(object):

_FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)')
_FORMAT_RE = re.compile('(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|X)')
_ESCAPE_RE = re.compile('\[[^\[\]]*\]')

_ONE_THROUGH_SIX_DIGIT_RE = re.compile('\d{1,6}')
_ONE_THROUGH_FIVE_DIGIT_RE = re.compile('\d{1,5}')
_ONE_THROUGH_FOUR_DIGIT_RE = re.compile('\d{1,4}')
_ONE_TWO_OR_THREE_DIGIT_RE = re.compile('\d{1,3}')
_ONE_OR_MORE_DIGIT_RE = re.compile('\d+')
_ONE_OR_TWO_DIGIT_RE = re.compile('\d{1,2}')
_FOUR_DIGIT_RE = re.compile('\d{4}')
_TWO_DIGIT_RE = re.compile('\d{2}')
Expand All @@ -47,12 +44,7 @@ class DateTimeParser(object):
'ZZZ': _TZ_NAME_RE,
'ZZ': _TZ_RE,
'Z': _TZ_RE,
'SSSSSS': _ONE_THROUGH_SIX_DIGIT_RE,
'SSSSS': _ONE_THROUGH_FIVE_DIGIT_RE,
'SSSS': _ONE_THROUGH_FOUR_DIGIT_RE,
'SSS': _ONE_TWO_OR_THREE_DIGIT_RE,
'SS': _ONE_OR_TWO_DIGIT_RE,
'S': re.compile('\d'),
'S': _ONE_OR_MORE_DIGIT_RE,
}

MARKERS = ['YYYY', 'MM', 'DD']
Expand Down Expand Up @@ -92,11 +84,10 @@ def parse_iso(self, string):
time_parts = re.split('[+-]', time_string, 1)
has_tz = len(time_parts) > 1
has_seconds = time_parts[0].count(':') > 1
has_subseconds = '.' in time_parts[0]
has_subseconds = re.search('[.,]', time_parts[0])

if has_subseconds:
subseconds_token = 'S' * min(len(re.split('\D+', time_parts[0].split('.')[1], 1)[0]), 6)
formats = ['YYYY-MM-DDTHH:mm:ss.%s' % subseconds_token]
formats = ['YYYY-MM-DDTHH:mm:ss%sS' % has_subseconds.group()]
elif has_seconds:
formats = ['YYYY-MM-DDTHH:mm:ss']
else:
Expand Down Expand Up @@ -132,6 +123,8 @@ def parse(self, string, fmt):

# Extract the bracketed expressions to be reinserted later.
escaped_fmt = re.sub(self._ESCAPE_RE, "#" , fmt)
# Any number of S is the same as one.
escaped_fmt = re.sub('S+', 'S', escaped_fmt)
escaped_data = re.findall(self._ESCAPE_RE, fmt)

fmt_pattern = escaped_fmt
Expand Down Expand Up @@ -202,18 +195,22 @@ def _parse_token(self, token, value, parts):
elif token in ['ss', 's']:
parts['second'] = int(value)

elif token == 'SSSSSS':
parts['microsecond'] = int(value)
elif token == 'SSSSS':
parts['microsecond'] = int(value) * 10
elif token == 'SSSS':
parts['microsecond'] = int(value) * 100
elif token == 'SSS':
parts['microsecond'] = int(value) * 1000
elif token == 'SS':
parts['microsecond'] = int(value) * 10000
elif token == 'S':
parts['microsecond'] = int(value) * 100000
# We have the *most significant* digits of an arbitrary-precision integer.
# We want the six most significant digits as an integer, rounded.
# FIXME: add nanosecond support somehow?
value = value.ljust(7, str('0'))

# floating-point (IEEE-754) defaults to half-to-even rounding
seventh_digit = int(value[6])
if seventh_digit == 5:
rounding = int(value[5]) % 2
elif seventh_digit > 5:
rounding = 1
else:
rounding = 0

parts['microsecond'] = int(value[:6]) + rounding

elif token == 'X':
parts['timestamp'] = int(value)
Expand Down Expand Up @@ -263,7 +260,7 @@ def _parse_multiformat(self, string, formats):
try:
_datetime = self.parse(string, fmt)
break
except:
except ParserError:
pass

if _datetime is None:
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ['_build', '_themes']

# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
Expand Down Expand Up @@ -123,7 +123,7 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = []

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
Expand Down
13 changes: 5 additions & 8 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ Call datetime functions that return properties:
Replace & shift
===============

Get a new :class:`Arrow <arrow.Arrow>` object, with altered attributes, just as you would with a datetime:
Get a new :class:`Arrow <arrow.arrow.Arrow>` object, with altered attributes, just as you would with a datetime:

.. code-block:: python
Expand Down Expand Up @@ -429,13 +429,9 @@ Use the following tokens in parsing and formatting. Note that they're not the s
+--------------------------------+--------------+-------------------------------------------+
| |s |0, 1, 2 ... 58, 59 |
+--------------------------------+--------------+-------------------------------------------+
|**Sub-second** |SSS |000, 001, 002 ... 998, 999 |
|**Sub-second** |S... |0, 02, 003, 000006, 123123123123... [#t3]_ |
+--------------------------------+--------------+-------------------------------------------+
| |SS |00, 01, 02 ... 98, 99 |
+--------------------------------+--------------+-------------------------------------------+
| |S |0, 1, 2 ... 8, 9 |
+--------------------------------+--------------+-------------------------------------------+
|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t3]_ |
|**Timezone** |ZZZ |Asia/Baku, Europe/Warsaw, GMT ... [#t4]_ |
+--------------------------------+--------------+-------------------------------------------+
| |ZZ |-07:00, -06:00 ... +06:00, +07:00 |
+--------------------------------+--------------+-------------------------------------------+
Expand All @@ -448,7 +444,8 @@ Use the following tokens in parsing and formatting. Note that they're not the s

.. [#t1] localization support for parsing and formatting
.. [#t2] localization support only for formatting
.. [#t3] timezone names from `tz database <https://www.iana.org/time-zones>`_ provided via dateutil package
.. [#t3] the result is truncated to microseconds, with `half-to-even rounding <https://en.wikipedia.org/wiki/IEEE_floating_point#Roundings_to_nearest>`_.
.. [#t4] timezone names from `tz database <https://www.iana.org/time-zones>`_ provided via dateutil package
---------
API Guide
Expand Down
6 changes: 3 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ verbosity = 2
all-modules = true
with-coverage = true
cover-min-percentage = 100
cover-package = arrow
cover-package =
arrow
tests
cover-erase = true
cover-inclusive = true
cover-branches = true

[bdist_wheel]
universal=1
2 changes: 1 addition & 1 deletion tests/arrow_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ def test_span_second(self):
assertEqual(floor, datetime(2013, 2, 15, 3, 41, 22, tzinfo=tz.tzutc()))
assertEqual(ceil, datetime(2013, 2, 15, 3, 41, 22, 999999, tzinfo=tz.tzutc()))

def test_span_hour(self):
def test_span_microsecond(self):

floor, ceil = self.arrow.span('microsecond')

Expand Down
7 changes: 6 additions & 1 deletion tests/factory_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,12 @@ def test_three_args(self):
assertEqual(self.factory.get(2013, 1, 1), datetime(2013, 1, 1, tzinfo=tz.tzutc()))


def UtcNowTests(Chai):
class UtcNowTests(Chai):

def setUp(self):
super(UtcNowTests, self).setUp()

self.factory = factory.ArrowFactory()

def test_utcnow(self):

Expand Down
84 changes: 72 additions & 12 deletions tests/parser_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def test_parse_multiformat(self):

mock_datetime = mock()

expect(self.parser.parse).args('str', 'fmt_a').raises(Exception)
expect(self.parser.parse).args('str', 'fmt_a').raises(ParserError)
expect(self.parser.parse).args('str', 'fmt_b').returns(mock_datetime)

result = self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b'])
Expand All @@ -31,10 +31,20 @@ def test_parse_multiformat(self):

def test_parse_multiformat_all_fail(self):

expect(self.parser.parse).args('str', 'fmt_a').raises(Exception)
expect(self.parser.parse).args('str', 'fmt_b').raises(Exception)
expect(self.parser.parse).args('str', 'fmt_a').raises(ParserError)
expect(self.parser.parse).args('str', 'fmt_b').raises(ParserError)

with assertRaises(Exception):
with assertRaises(ParserError):
self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b'])

def test_parse_multiformat_unexpected_fail(self):

class UnexpectedError(Exception):
pass

expect(self.parser.parse).args('str', 'fmt_a').raises(UnexpectedError)

with assertRaises(UnexpectedError):
self.parser._parse_multiformat('str', ['fmt_a', 'fmt_b'])

def test_parse_token_nonsense(self):
Expand Down Expand Up @@ -188,17 +198,30 @@ def test_parse_subsecond(self):
assertEqual(self.parser.parse('2013-01-01 12:30:45.987654', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected)
assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987654'), expected)

def test_parse_subsecond_rounding(self):
expected = datetime(2013, 1, 1, 12, 30, 45, 987654)
assertEqual(self.parser.parse('2013-01-01 12:30:45.9876543', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected)
assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.9876543'), expected)
format = 'YYYY-MM-DD HH:mm:ss.S'

expected = datetime(2013, 1, 1, 12, 30, 45, 987654)
assertEqual(self.parser.parse('2013-01-01 12:30:45.98765432', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected)
assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.98765432'), expected)
# round up
string = '2013-01-01 12:30:45.9876539'
assertEqual(self.parser.parse(string, format), expected)
assertEqual(self.parser.parse_iso(string), expected)

expected = datetime(2013, 1, 1, 12, 30, 45, 987654)
assertEqual(self.parser.parse('2013-01-01 12:30:45.987654321', 'YYYY-MM-DD HH:mm:ss.SSSSSS'), expected)
assertEqual(self.parser.parse_iso('2013-01-01 12:30:45.987654321'), expected)
# round down
string = '2013-01-01 12:30:45.98765432'
assertEqual(self.parser.parse(string, format), expected)
#import pudb; pudb.set_trace()
assertEqual(self.parser.parse_iso(string), expected)

# round half-up
string = '2013-01-01 12:30:45.987653521'
assertEqual(self.parser.parse(string, format), expected)
assertEqual(self.parser.parse_iso(string), expected)

# round half-down
string = '2013-01-01 12:30:45.9876545210'
assertEqual(self.parser.parse(string, format), expected)
assertEqual(self.parser.parse_iso(string), expected)

def test_map_lookup_keyerror(self):

Expand Down Expand Up @@ -398,6 +421,21 @@ def test_YYYY_MM_DDTHH_mm_ss_S(self):
datetime(2013, 2, 3, 4, 5, 6, 789120)
)

# ISO 8601:2004(E), ISO, 2004-12-01, 4.2.2.4 ... the decimal fraction
# shall be divided from the integer part by the decimal sign specified
# in ISO 31-0, i.e. the comma [,] or full stop [.]. Of these, the comma
# is the preferred sign.
assertEqual(
self.parser.parse_iso('2013-02-03T04:05:06,789123678'),
datetime(2013, 2, 3, 4, 5, 6, 789124)
)

# there is no limit on the number of decimal places
assertEqual(
self.parser.parse_iso('2013-02-03T04:05:06.789123678'),
datetime(2013, 2, 3, 4, 5, 6, 789124)
)

def test_YYYY_MM_DDTHH_mm_ss_SZ(self):

assertEqual(
Expand Down Expand Up @@ -431,6 +469,28 @@ def test_YYYY_MM_DDTHH_mm_ss_SZ(self):
datetime(2013, 2, 3, 4, 5, 6, 789120)
)

def test_gnu_date(self):
"""
regression tests for parsing output from GNU date(1)
"""
# date -Ins
assertEqual(
self.parser.parse_iso('2016-11-16T09:46:30,895636557-0800'),
datetime(
2016, 11, 16, 9, 46, 30, 895636,
tzinfo=tz.tzoffset(None, -3600 * 8),
)
)

# date --rfc-3339=ns
assertEqual(
self.parser.parse_iso('2016-11-16 09:51:14.682141526-08:00'),
datetime(
2016, 11, 16, 9, 51, 14, 682142,
tzinfo=tz.tzoffset(None, -3600 * 8),
)
)

def test_isoformat(self):

dt = datetime.utcnow()
Expand Down
2 changes: 1 addition & 1 deletion tests/util_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_total_seconds_26(self):

assertEqual(util._total_seconds_26(td), 30)

if util.version >= '2.7':
if util.version >= '2.7': # pragma: no cover

def test_total_seconds_27(self):

Expand Down

0 comments on commit 5f235bc

Please sign in to comment.