Ceil/floor function re-implementation using bisect. Corrections and updates to unittests and documentation.

This commit is contained in:
Adam Gibson 2015-04-27 17:16:54 +08:00
parent b175ab7a21
commit b6f61b8d8b
3 changed files with 71 additions and 27 deletions

View File

@ -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))
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)

View File

@ -8,4 +8,11 @@ Alternative Ceiling/Floor Functions
.. autofunction:: boltons.mathutils.ceil_from_iter
.. autofunction:: boltons.mathutils.floor_from_iter
.. 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

View File

@ -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()