Repository: sdispater/pendulum
Pull request #139 (2017-08-26 08:37:54)

_compat.py Use feature detection instead of version detection
https://docs.python.org/3/howto/pyporting.html#use-feature-detection-instead-of-version-detection

On `encodings`, given the `return` statement, all other _falsey_ values are just as dangerous as `None`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12701ee..f33c32c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Change Log
 
+## [Unreleased]
+
+### Fixed
+
+- Fixed normalization of microseconds in durations.
+- Fixed microseconds not being included in `average()`. (Thanks to [ericfrederich](https://github.com/ericfrederich)) 
+
+
 ## [1.2.4] - 2017-06-20
 
 ### Fixed
@@ -26,10 +34,10 @@
 ## [1.2.1] - 2017-05-23
 
 ### Fixed
-
 - Fixed incorrect `fold` attribute on Python 3.6 when not passing a timezone. (Thanks to [neonquill](https://github.com/neonquill))
 
 
+
 ## [1.2.0] - 2017-03-24
 
 ### Added
diff --git a/README.rst b/README.rst
index 40403e1..330965a 100644
--- a/README.rst
+++ b/README.rst
@@ -10,7 +10,7 @@ Pendulum
 .. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg
     :target: https://codecov.io/gh/sdispater/pendulum/branch/master
 
-.. image:: https://travis-ci.org/sdispater/pendulum.png
+.. image:: https://travis-ci.org/sdispater/pendulum.svg
     :alt: Pendulum Build status
     :target: https://travis-ci.org/sdispater/pendulum
 
diff --git a/docs/_docs/difference.rst b/docs/_docs/difference.rst
index 9e56f01..53c8570 100644
--- a/docs/_docs/difference.rst
+++ b/docs/_docs/difference.rst
@@ -83,7 +83,7 @@ You may also pass ``True`` as a 2nd parameter to remove the modifiers `ago`, `fr
     # The most typical usage is for comments
     # The instance is the date the comment was created
     # and its being compared to default now()
-    pendulum.now().subtract(dayss=1).diff_for_humans()
+    pendulum.now().subtract(days=1).diff_for_humans()
     '5 days ago'
 
     pendulum.now().diff_for_humans(Pendulum.now().subtract(years=1))
diff --git a/pendulum/_compat.py b/pendulum/_compat.py
index 48c0aec..b722df3 100644
--- a/pendulum/_compat.py
+++ b/pendulum/_compat.py
@@ -4,23 +4,22 @@ import sys
 
 PY2 = sys.version_info[0] == 2
 PY3K = sys.version_info[0] >= 3
-PY33 = sys.version_info >= (3, 3)
 PY36 = sys.version_info >= (3, 6)
 
 
-if PY2:
-    long = long
-    unicode = unicode
-    basestring = basestring
-else:
+try:               # Python 2
+    long
+    unicode
+    basestring
+except NameError:  # Python 3
     long = int
     unicode = str
     basestring = str
 
-if PY33:
+try:               # Python >= 3.3
     FileNotFoundError = FileNotFoundError
-else:
-    FileNotFoundError = IOError # cf PEP-3151 
+except NameError:  # Python < 3.3
+    FileNotFoundError = IOError  # cf PEP-3151 
 
 def decode(string, encodings=None):
     if not PY2 and not isinstance(string, bytes):
@@ -29,8 +28,7 @@ def decode(string, encodings=None):
     if PY2 and isinstance(string, unicode):
         return string
 
-    if encodings is None:
-        encodings = ['utf-8', 'latin1', 'ascii']
+    encodings = encodings or ['utf-8', 'latin1', 'ascii']
 
     for encoding in encodings:
         try:
@@ -48,8 +46,7 @@ def encode(string, encodings=None):
     if PY2 and isinstance(string, str):
         return string
 
-    if encodings is None:
-        encodings = ['utf-8', 'latin1', 'ascii']
+    encodings = encodings or ['utf-8', 'latin1', 'ascii']
 
     for encoding in encodings:
         try:
diff --git a/pendulum/interval.py b/pendulum/interval.py
index e8c1d97..1c26a1e 100644
--- a/pendulum/interval.py
+++ b/pendulum/interval.py
@@ -60,7 +60,7 @@ class BaseInterval(timedelta):
         if total < 0:
             m = -1
 
-        self._microseconds = abs(round(total % 1 * 1e6)) * m
+        self._microseconds = round(total % m * 1e6)
         self._seconds = abs(int(total)) % SECONDS_PER_DAY * m
         self._days = abs(int(total)) // SECONDS_PER_DAY * m
 
diff --git a/pendulum/pendulum.py b/pendulum/pendulum.py
index 247112a..96ffd95 100644
--- a/pendulum/pendulum.py
+++ b/pendulum/pendulum.py
@@ -1769,7 +1769,8 @@ class Pendulum(Date, datetime.datetime):
         if dt is None:
             dt = Pendulum.now(self._tz)
 
-        return self.add(seconds=int(self.diff(dt, False).in_seconds() / 2))
+        diff = self.diff(dt, False)
+        return self.add(microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2)
 
     def _get_datetime(self, value, pendulum=False):
         """


Test patch:
diff --git a/tests/pendulum_tests/test_diff.py b/tests/pendulum_tests/test_diff.py
index 316439d..0da9a7f 100644
--- a/tests/pendulum_tests/test_diff.py
+++ b/tests/pendulum_tests/test_diff.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 
+import pendulum
+
 from datetime import datetime
 from contextlib import contextmanager
 from pendulum import Pendulum
@@ -593,3 +595,13 @@ class DiffTest(AbstractTestCase):
 
         self.assertEqual(3600, (future - d).total_seconds())
         self.assertEqual(3600, (future_dt - d).total_seconds())
+
+    def test_normalization(self):
+        d1 = pendulum.create(2012, 1, 1, 1, 2, 3, 123456)
+        d2 = pendulum.create(2011, 12, 31, 22, 2, 3)
+        delta = d2 - d1
+
+        assert delta.days == 0
+        assert delta.seconds == -10800
+        assert delta.microseconds == -123456
+        assert d1 + delta == d2
diff --git a/tests/pendulum_tests/test_start_end_of.py b/tests/pendulum_tests/test_start_end_of.py
index fbc034c..f3ff332 100644
--- a/tests/pendulum_tests/test_start_end_of.py
+++ b/tests/pendulum_tests/test_start_end_of.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 
-from pendulum import Pendulum
+from itertools import product, tee
 
+from pendulum import Pendulum
 from .. import AbstractTestCase
 
 
@@ -208,19 +209,70 @@ class StartEndOfTest(AbstractTestCase):
         self.assertIsInstanceOfPendulum(d)
 
     def test_average_from_same(self):
-        d1 = Pendulum.create(2000, 1, 31, 2, 3, 4)
-        d2 = Pendulum.create(2000, 1, 31, 2, 3, 4).average(d1)
-        self.assertPendulum(d2, 2000, 1, 31, 2, 3, 4)
+        d1 = Pendulum.create(2000, 1, 31, 2, 3, 4, 5)
+        d2 = Pendulum.create(2000, 1, 31, 2, 3, 4, 5).average(d1)
+        self.assertPendulum(d2, 2000, 1, 31, 2, 3, 4, 5)
 
     def test_average_from_greater(self):
-        d1 = Pendulum.create(2000, 1, 1, 1, 1, 1, tz='local')
-        d2 = Pendulum.create(2009, 12, 31, 23, 59, 59, tz='local').average(d1)
-        self.assertPendulum(d2, 2004, 12, 31, 12, 30, 30)
+        d1 = Pendulum.create(2000, 1, 1, 1, 1, 1, 1, tz='local')
+        d2 = Pendulum.create(2009, 12, 31, 23, 59, 59, 999999, tz='local').average(d1)
+        self.assertPendulum(d2, 2004, 12, 31, 12, 30, 30, 500000)
 
     def test_average_from_lower(self):
-        d1 = Pendulum.create(2009, 12, 31, 23, 59, 59, tz='local')
-        d2 = Pendulum.create(2000, 1, 1, 1, 1, 1, tz='local').average(d1)
-        self.assertPendulum(d2, 2004, 12, 31, 12, 30, 30)
+        d1 = Pendulum.create(2009, 12, 31, 23, 59, 59, 999999, tz='local')
+        d2 = Pendulum.create(2000, 1, 1, 1, 1, 1, 1, tz='local').average(d1)
+        self.assertPendulum(d2, 2004, 12, 31, 12, 30, 30, 500000)
+
+    def test_average_with_microseconds(self):
+        for d1, d2, expected_average in [
+            # 3ms and 5ms should average to 4
+            ((1982, 12, 4, 17, 13, 0, 3), (1982, 12, 4, 17, 13, 0, 5), (1982, 12, 4, 17, 13, 0, 4)),
+            # 999999ms and subsequent 1ms should average to 0
+            ((1982, 12, 4, 0, 0, 0, 999999), (1982, 12, 4, 0, 0, 1, 1), (1982, 12, 4, 0, 0, 1, 0)),
+            # 1.000002 should average to 0.500001
+            ((2000, 1, 1, 0, 0, 0, 0), (2000, 1, 1, 0, 0, 1, 2), (2000, 1, 1, 0, 0, 0, 500001)),
+        ]:
+            d1 = Pendulum.create(*d1)
+            d2 = Pendulum.create(*d2)
+            a1 = d1.average(d2)
+            a2 = d2.average(d1)
+            self.assertPendulum(a1, *expected_average)
+            self.assertPendulum(a2, *expected_average)
+
+    def test_average_commutative(self):
+        for d1, d2 in product(*tee([
+            Pendulum.create(1981, 10,  9,  1,  2,  3,  4),
+            Pendulum.create(1982, 12,  4,  5,  6,  7,  8),
+            Pendulum.create(2013,  4, 13,  9, 10, 11, 12),
+            Pendulum.create(1982,  9, 24, 13, 14, 15, 16),
+        ])):
+            a1 = d1.average(d2)
+            a2 = d2.average(d1)
+            self.assertEqual(a1, a2)
+
+    def test_bisect_creates_all_times(self):
+
+        def bisect(start, end):
+            # should generate, in order, all microseconds between start and end
+            mid = start.average(end)
+            if mid == start or mid == end:
+                return
+            for d in bisect(start, mid):
+                yield d
+            yield mid
+            for d in bisect(mid, end):
+                yield d
+
+        start = Pendulum.now()
+        end = start.add(microseconds=1000)
+        d_next = start.copy()
+        n_values = 0
+        for d in bisect(start, end):
+            n_values += 1
+            d_next = d_next.add(microseconds=1)
+            self.assertEqual(d, d_next)
+
+        self.assertEqual(n_values, 999)
 
     def start_of_with_invalid_unit(self):
         self.assertRaises(ValueError, Pendulum.now().start_of('invalid'))