mirror of https://github.com/mahmoud/boltons.git
250 lines
7.8 KiB
Python
250 lines
7.8 KiB
Python
# Copyright (c) 2013, Mahmoud Hashemi
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are
|
|
# met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
#
|
|
# * Redistributions in binary form must reproduce the above
|
|
# copyright notice, this list of conditions and the following
|
|
# disclaimer in the documentation and/or other materials provided
|
|
# with the distribution.
|
|
#
|
|
# * The names of the contributors may not be used to endorse or
|
|
# promote products derived from this software without specific
|
|
# prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
"""This module provides useful math functions on top of Python's
|
|
built-in :mod:`math` module.
|
|
"""
|
|
|
|
from math import ceil as _ceil, floor as _floor
|
|
import bisect
|
|
import binascii
|
|
|
|
|
|
def clamp(x, lower=float('-inf'), upper=float('inf')):
|
|
"""Limit a value to a given range.
|
|
|
|
Args:
|
|
x (int or float): Number to be clamped.
|
|
lower (int or float): Minimum value for x.
|
|
upper (int or float): Maximum value for x.
|
|
|
|
The returned value is guaranteed to be between *lower* and
|
|
*upper*. Integers, floats, and other comparable types can be
|
|
mixed.
|
|
|
|
>>> clamp(1.0, 0, 5)
|
|
1.0
|
|
>>> clamp(-1.0, 0, 5)
|
|
0
|
|
>>> clamp(101.0, 0, 5)
|
|
5
|
|
>>> clamp(123, upper=5)
|
|
5
|
|
|
|
Similar to `numpy's clip`_ function.
|
|
|
|
.. _numpy's clip: http://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html
|
|
|
|
"""
|
|
if upper < lower:
|
|
raise ValueError('expected upper bound (%r) >= lower bound (%r)'
|
|
% (upper, lower))
|
|
return min(max(x, lower), upper)
|
|
|
|
|
|
def ceil(x, options=None):
|
|
"""Return the ceiling of *x*. If *options* is set, return the smallest
|
|
integer or float from *options* that is greater than or equal to
|
|
*x*.
|
|
|
|
Args:
|
|
x (int or float): Number to be tested.
|
|
options (iterable): Optional iterable of arbitrary numbers
|
|
(ints or floats).
|
|
|
|
>>> VALID_CABLE_CSA = [1.5, 2.5, 4, 6, 10, 25, 35, 50]
|
|
>>> ceil(3.5, options=VALID_CABLE_CSA)
|
|
4
|
|
>>> ceil(4, options=VALID_CABLE_CSA)
|
|
4
|
|
"""
|
|
if options is None:
|
|
return _ceil(x)
|
|
options = sorted(options)
|
|
i = bisect.bisect_left(options, x)
|
|
if i == len(options):
|
|
raise ValueError("no ceil options greater than or equal to: %r" % x)
|
|
return options[i]
|
|
|
|
|
|
def floor(x, options=None):
|
|
"""Return the floor of *x*. If *options* is set, return the largest
|
|
integer or float from *options* that is less than or equal to
|
|
*x*.
|
|
|
|
Args:
|
|
x (int or float): Number to be tested.
|
|
options (iterable): Optional iterable of arbitrary numbers
|
|
(ints or floats).
|
|
|
|
>>> VALID_CABLE_CSA = [1.5, 2.5, 4, 6, 10, 25, 35, 50]
|
|
>>> floor(3.5, options=VALID_CABLE_CSA)
|
|
2.5
|
|
>>> floor(2.5, options=VALID_CABLE_CSA)
|
|
2.5
|
|
|
|
"""
|
|
if options is None:
|
|
return _floor(x)
|
|
options = sorted(options)
|
|
|
|
i = bisect.bisect_right(options, x)
|
|
if not i:
|
|
raise ValueError("no floor options less than or equal to: %r" % x)
|
|
return options[i - 1]
|
|
|
|
|
|
class Bits:
|
|
'''
|
|
An immutable bit-string or bit-array object.
|
|
Provides list-like access to bits as bools,
|
|
as well as bitwise masking and shifting operators.
|
|
Bits also make it easy to convert between many
|
|
different useful representations:
|
|
|
|
* bytes -- good for serializing raw binary data
|
|
* int -- good for incrementing (e.g. to try all possible values)
|
|
* list of bools -- good for iterating over or treating as flags
|
|
* hex/bin string -- good for human readability
|
|
|
|
'''
|
|
__slots__ = ('val', 'len')
|
|
|
|
def __init__(self, val=0, len_=None):
|
|
if type(val) is not int:
|
|
if type(val) is list:
|
|
val = ''.join(['1' if e else '0' for e in val])
|
|
if type(val) is bytes:
|
|
val = val.decode('ascii')
|
|
if type(val) is str:
|
|
if len_ is None:
|
|
len_ = len(val)
|
|
if val.startswith('0x'):
|
|
len_ = (len_ - 2) * 4
|
|
if val.startswith('0x'):
|
|
val = int(val, 16)
|
|
else:
|
|
if val:
|
|
val = int(val, 2)
|
|
else:
|
|
val = 0
|
|
if type(val) is not int:
|
|
raise TypeError(f'initialized with bad type: {type(val).__name__}')
|
|
if val < 0:
|
|
raise ValueError('Bits cannot represent negative values')
|
|
if len_ is None:
|
|
len_ = len(f'{val:b}')
|
|
if val > 2 ** len_:
|
|
raise ValueError(f'value {val} cannot be represented with {len_} bits')
|
|
self.val = val # data is stored internally as integer
|
|
self.len = len_
|
|
|
|
def __getitem__(self, k):
|
|
if type(k) is slice:
|
|
return Bits(self.as_bin()[k])
|
|
if type(k) is int:
|
|
if k >= self.len:
|
|
raise IndexError(k)
|
|
return bool((1 << (self.len - k - 1)) & self.val)
|
|
raise TypeError(type(k))
|
|
|
|
def __len__(self):
|
|
return self.len
|
|
|
|
def __eq__(self, other):
|
|
if type(self) is not type(other):
|
|
return NotImplemented
|
|
return self.val == other.val and self.len == other.len
|
|
|
|
def __or__(self, other):
|
|
if type(self) is not type(other):
|
|
return NotImplemented
|
|
return Bits(self.val | other.val, max(self.len, other.len))
|
|
|
|
def __and__(self, other):
|
|
if type(self) is not type(other):
|
|
return NotImplemented
|
|
return Bits(self.val & other.val, max(self.len, other.len))
|
|
|
|
def __lshift__(self, other):
|
|
return Bits(self.val << other, self.len + other)
|
|
|
|
def __rshift__(self, other):
|
|
return Bits(self.val >> other, self.len - other)
|
|
|
|
def __hash__(self):
|
|
return hash(self.val)
|
|
|
|
def as_list(self):
|
|
return [c == '1' for c in self.as_bin()]
|
|
|
|
def as_bin(self):
|
|
return f'{{0:0{self.len}b}}'.format(self.val)
|
|
|
|
def as_hex(self):
|
|
# make template to pad out to number of bytes necessary to represent bits
|
|
tmpl = f'%0{2 * (self.len // 8 + ((self.len % 8) != 0))}X'
|
|
ret = tmpl % self.val
|
|
return ret
|
|
|
|
def as_int(self):
|
|
return self.val
|
|
|
|
def as_bytes(self):
|
|
return binascii.unhexlify(self.as_hex())
|
|
|
|
@classmethod
|
|
def from_list(cls, list_):
|
|
return cls(list_)
|
|
|
|
@classmethod
|
|
def from_bin(cls, bin):
|
|
return cls(bin)
|
|
|
|
@classmethod
|
|
def from_hex(cls, hex):
|
|
if isinstance(hex, bytes):
|
|
hex = hex.decode('ascii')
|
|
if not hex.startswith('0x'):
|
|
hex = '0x' + hex
|
|
return cls(hex)
|
|
|
|
@classmethod
|
|
def from_int(cls, int_, len_=None):
|
|
return cls(int_, len_)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, bytes_):
|
|
return cls.from_hex(binascii.hexlify(bytes_))
|
|
|
|
def __repr__(self):
|
|
cn = self.__class__.__name__
|
|
return f"{cn}('{self.as_bin()}')"
|