From b6f61b8d8b46ad87f69db5bec038e1e21dd0ed6b Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Mon, 27 Apr 2015 17:16:54 +0800 Subject: [PATCH] Ceil/floor function re-implementation using bisect. Corrections and updates to unittests and documentation. --- boltons/mathutils.py | 45 +++++++++++++++++++++++++++++++++-------- docs/mathutils.rst | 9 ++++++++- tests/mathutils_test.py | 44 +++++++++++++++++++++++----------------- 3 files changed, 71 insertions(+), 27 deletions(-) diff --git a/boltons/mathutils.py b/boltons/mathutils.py index a95b076..ffb3803 100644 --- a/boltons/mathutils.py +++ b/boltons/mathutils.py @@ -1,15 +1,18 @@ """This module provides useful math functions on top of Python's built-in :mod:`math` module. """ +import bisect -def ceil_from_iter(src, x, allow_equal=True): +def ceil_from_iter(src, x, allow_equal=True, sorted_src=False): """ Return the ceiling of *x*, the smallest integer or float from *src* that is greater than [or equal to] *x*. Args: - src (iterable): Iterable of arbitrary numbers (ints or floats). - x (int or float): Number to be tested. + src (iterable): Iterable of arbitrary numbers (ints or floats). + x (int or float): Number to be tested. allow_equal (bool): Defaults to ``True``. Allows equality to be ignored if set to ``False``. + sorted_src (bool): Defaults to ``False``. Allows potential performance increase for large *src* iterable if set + to ``False``, as long as *src* is pre-sorted. >>> VALID_CABLE_CSA = [1.5, 2.5, 4, 6, 10, 25, 35, 50] >>> ceil_from_iter(VALID_CABLE_CSA, 3.5) @@ -20,17 +23,31 @@ def ceil_from_iter(src, x, allow_equal=True): 6 """ - return min(filter(lambda y: y >= x if allow_equal else y > x, src)) + if not sorted_src: + src = sorted(src) + + if allow_equal: + i = bisect.bisect_left(src, x) + if i != len(src): + return src[i] + raise ValueError("No ceiling value in source iterable greater than or equal to:", x) + else: + i = bisect.bisect_right(src, x) + if i != len(src): + return src[i] + raise ValueError("No ceiling value in source iterable greater than:", x) -def floor_from_iter(src, x, allow_equal=True): +def floor_from_iter(src, x, allow_equal=True, sorted_src=False): """ Return the floor of *x*, the largest integer or float from *src* that is less than [or equal to] *x*. Args: - src (iterable): Iterable of arbitrary numbers (ints or floats). - x (int or float): Number to be tested. + src (iterable): Iterable of arbitrary numbers (ints or floats). + x (int or float): Number to be tested. allow_equal (bool): Defaults to ``True``. Allows equality to be ignored if set to ``False``. + sorted_src (bool): Defaults to ``False``. Allows potential performance increase for large *src* iterable if set + to ``False``, as long as *src* is pre-sorted. >>> VALID_CABLE_CSA = [1.5, 2.5, 4, 6, 10, 25, 35, 50] >>> floor_from_iter(VALID_CABLE_CSA, 3.5) @@ -41,4 +58,16 @@ def floor_from_iter(src, x, allow_equal=True): 1.5 """ - return max(filter(lambda y: y <= x if allow_equal else y < x, src)) \ No newline at end of file + if not sorted_src: + src = sorted(src) + + if allow_equal: + i = bisect.bisect_right(src, x) + if i: + return src[i-1] + raise ValueError("No floor value in source iterable less than or equal to:", x) + else: + i = bisect.bisect_left(src, x) + if i: + return src[i-1] + raise ValueError("No floor value in source iterable less than:", x) \ No newline at end of file diff --git a/docs/mathutils.rst b/docs/mathutils.rst index b3df524..c66d2b6 100644 --- a/docs/mathutils.rst +++ b/docs/mathutils.rst @@ -8,4 +8,11 @@ Alternative Ceiling/Floor Functions .. autofunction:: boltons.mathutils.ceil_from_iter -.. autofunction:: boltons.mathutils.floor_from_iter \ No newline at end of file +.. autofunction:: boltons.mathutils.floor_from_iter + +Note: :func:`ceil_from_iter` and :func:`floor_from_iter` functions are based on `this example`_ using from the +:mod:`bisect` module in the standard library. Refer to this `StackOverflow Answer`_ for further information regarding +the performance impact of this approach. + +.. _this example: https://docs.python.org/3/library/bisect.html#searching-sorted-lists +.. _StackOverflow Answer: http://stackoverflow.com/a/12141511/811740 \ No newline at end of file diff --git a/tests/mathutils_test.py b/tests/mathutils_test.py index bafcbff..a2cec50 100644 --- a/tests/mathutils_test.py +++ b/tests/mathutils_test.py @@ -10,14 +10,15 @@ class TestCeilAndFloor(unittest.TestCase): self.BIG_LIST_SORTED = sorted(self.BIG_LIST) self.OUT_OF_RANGE_LOWER = 60 self.OUT_OF_RANGE_UPPER = 2500 - self.VALID_LOWER = self.BIG_LIST_SORTED[3] - self.VALID_UPPER = self.BIG_LIST_SORTED[-3] - self.VAILD_BETWEEN = 248.5 + self.VALID_LOWER = 247 + self.VALID_UPPER = 2314 + self.VALID_BETWEEN = 248.5 # Tests for boltons.mathutils.ceil_from_iter() def test_ceil_from_iter_default(self): self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_LOWER), self.VALID_LOWER) self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_UPPER), self.VALID_UPPER) + self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_BETWEEN), 250) def test_ceil_from_iter_default_allow_equal_false(self): self.assertNotEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_LOWER, allow_equal=False), self.VALID_LOWER) @@ -30,18 +31,18 @@ class TestCeilAndFloor(unittest.TestCase): self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_UPPER), bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER)) - def test_ceil_from_iter_unsorted_failure(self): - self.assertNotEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_LOWER), - bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_LOWER, sorted_src=True)) + def test_ceil_from_iter_sorted_src_true(self): + self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_LOWER), + bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_LOWER, sorted_src=True)) - self.assertNotEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_UPPER), - bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER, sorted_src=True)) + self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST, self.VALID_UPPER), + bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER, sorted_src=True)) def test_ceil_from_iter_sorted(self): self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_LOWER), self.VALID_LOWER) self.assertEqual(bmu.ceil_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER), self.VALID_UPPER) - def test_ceil_from_iter_exception_out_of_range_lower(self): + def test_ceil_from_iter_out_of_range_lower(self): expected = min(self.BIG_LIST) actual = bmu.ceil_from_iter(self.BIG_LIST, self.OUT_OF_RANGE_LOWER) self.assertEqual(expected, actual) @@ -49,10 +50,14 @@ class TestCeilAndFloor(unittest.TestCase): def test_ceil_from_iter_exception_out_of_range_upper(self): self.assertRaises(ValueError, bmu.ceil_from_iter, self.BIG_LIST, self.OUT_OF_RANGE_UPPER) + def test_ceil_from_iter_exception_out_of_range_upper_allow_equal_false(self): + self.assertRaises(ValueError, bmu.ceil_from_iter, self.BIG_LIST, self.OUT_OF_RANGE_UPPER, allow_equal=False) + # Tests for boltons.mathutils.floor_from_iter() def test_floor_from_iter_default(self): self.assertEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_LOWER), self.VALID_LOWER) self.assertEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_UPPER), self.VALID_UPPER) + self.assertEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_BETWEEN), 247) def test_floor_from_iter_default_allow_equal_false(self): self.assertNotEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_LOWER, allow_equal=False), self.VALID_LOWER) @@ -65,24 +70,27 @@ class TestCeilAndFloor(unittest.TestCase): self.assertEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_UPPER), bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER)) - def test_floor_from_iter_unsorted_failure(self): - self.assertNotEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_LOWER), - bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_LOWER, sorted_src=True)) + def test_floor_from_iter_sorted_src_true(self): + self.assertEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_LOWER), + bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_LOWER, sorted_src=True)) - self.assertNotEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_UPPER), - bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER, sorted_src=True)) + self.assertEqual(bmu.floor_from_iter(self.BIG_LIST, self.VALID_UPPER), + bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER, sorted_src=True)) def test_floor_from_iter_sorted(self): self.assertEqual(bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_LOWER), self.VALID_LOWER) self.assertEqual(bmu.floor_from_iter(self.BIG_LIST_SORTED, self.VALID_UPPER), self.VALID_UPPER) - def test_floor_from_iter_exception_out_of_range_lower(self): - self.assertRaises(ValueError, bmu.floor_from_iter, self.BIG_LIST, self.OUT_OF_RANGE_LOWER) - - def test_floor_from_iter_exception_out_of_range_upper(self): + def test_floor_from_iter_out_of_range_upper(self): expected = max(self.BIG_LIST) actual = bmu.floor_from_iter(self.BIG_LIST, self.OUT_OF_RANGE_UPPER) self.assertEqual(expected, actual) + def test_floor_from_iter_exception_out_of_range_lower(self): + self.assertRaises(ValueError, bmu.floor_from_iter, self.BIG_LIST, self.OUT_OF_RANGE_LOWER) + + def test_floor_from_iter_exception_out_of_range_lower_allow_equal_false(self): + self.assertRaises(ValueError, bmu.floor_from_iter, self.BIG_LIST, self.OUT_OF_RANGE_LOWER, allow_equal=False) + if __name__ == "__main__": unittest.main() \ No newline at end of file