zjowowen's picture
init space
079c32c
import math
import operator
from typing import Optional, Union, Callable, Any
from .base import Loader, ILoaderClass
from .utils import keep, check_only
NUMBER_TYPES = (int, float)
NUMBER_TYPING = Union[int, float]
def numeric(int_ok: bool = True, float_ok: bool = True, inf_ok: bool = True) -> ILoaderClass:
"""
Overview:
Create a numeric loader.
Arguments:
- int_ok (:obj:`bool`): Whether int is allowed.
- float_ok (:obj:`bool`): Whether float is allowed.
- inf_ok (:obj:`bool`): Whether inf is allowed.
"""
if not int_ok and not float_ok:
raise ValueError('Either int or float should be allowed.')
def _load(value) -> NUMBER_TYPING:
if isinstance(value, NUMBER_TYPES):
if math.isnan(value):
raise ValueError('nan is not numeric value')
if isinstance(value, int) and not int_ok:
raise TypeError('int is not allowed but {actual} found'.format(actual=repr(value)))
if isinstance(value, float) and not float_ok:
raise TypeError('float is not allowed but {actual} found'.format(actual=repr(value)))
if math.isinf(value) and not inf_ok:
raise ValueError('inf is not allowed but {actual} found'.format(actual=repr(value)))
return value
else:
raise TypeError(
'numeric value should be either int, float or str, but {actual} found'.format(
actual=repr(type(value).__name__)
)
)
return Loader(_load)
def interval(
left: Optional[NUMBER_TYPING] = None,
right: Optional[NUMBER_TYPING] = None,
left_ok: bool = True,
right_ok: bool = True,
eps=0.0
) -> ILoaderClass:
"""
Overview:
Create a interval loader.
Arguments:
- left (:obj:`Optional[NUMBER_TYPING]`): The left bound.
- right (:obj:`Optional[NUMBER_TYPING]`): The right bound.
- left_ok (:obj:`bool`): Whether left bound is allowed.
- right_ok (:obj:`bool`): Whether right bound is allowed.
- eps (:obj:`float`): The epsilon.
"""
if left is None:
left = -math.inf
if right is None:
right = +math.inf
if left > right:
raise ValueError(
"Left bound should no more than right bound, but {left} > {right}.".format(
left=repr(left), right=repr(right)
)
)
eps = math.fabs(eps)
def _value_compare_with_eps(a, b) -> int:
if math.fabs(a - b) <= eps:
return 0
elif a < b:
return -1
else:
return 1
def _load(value) -> NUMBER_TYPING:
_left_check = _value_compare_with_eps(value, left)
if _left_check < 0:
raise ValueError(
'value should be no less than {left} but {value} found'.format(left=repr(left), value=repr(value))
)
elif not left_ok and _left_check == 0:
raise ValueError(
'value should not be equal to left bound {left} but {value} found'.format(
left=repr(left), value=repr(value)
)
)
_right_check = _value_compare_with_eps(value, right)
if _right_check > 0:
raise ValueError(
'value should be no more than {right} but {value} found'.format(right=repr(right), value=repr(value))
)
elif not right_ok and _right_check == 0:
raise ValueError(
'value should not be equal to right bound {right} but {value} found'.format(
right=repr(right), value=repr(value)
)
)
return value
return Loader(_load)
def is_negative() -> ILoaderClass:
"""
Overview:
Create a negative loader.
"""
return Loader((lambda x: x < 0, lambda x: ValueError('negative required but {value} found'.format(value=repr(x)))))
def is_positive() -> ILoaderClass:
"""
Overview:
Create a positive loader.
"""
return Loader((lambda x: x > 0, lambda x: ValueError('positive required but {value} found'.format(value=repr(x)))))
def non_negative() -> ILoaderClass:
"""
Overview:
Create a non-negative loader.
"""
return Loader(
(lambda x: x >= 0, lambda x: ValueError('non-negative required but {value} found'.format(value=repr(x))))
)
def non_positive() -> ILoaderClass:
"""
Overview:
Create a non-positive loader.
"""
return Loader(
(lambda x: x <= 0, lambda x: ValueError('non-positive required but {value} found'.format(value=repr(x))))
)
def negative() -> ILoaderClass:
"""
Overview:
Create a negative loader.
"""
return Loader(lambda x: -x)
def positive() -> ILoaderClass:
"""
Overview:
Create a positive loader.
"""
return Loader(lambda x: +x)
def _math_binary(func: Callable[[Any, Any], Any], attachment) -> ILoaderClass:
"""
Overview:
Create a math binary loader.
Arguments:
- func (:obj:`Callable[[Any, Any], Any]`): The function.
- attachment (:obj:`Any`): The attachment.
"""
return Loader(lambda x: func(x, Loader(attachment)(x)))
def plus(addend) -> ILoaderClass:
"""
Overview:
Create a plus loader.
Arguments:
- addend (:obj:`Any`): The addend.
"""
return _math_binary(lambda x, y: x + y, addend)
def minus(subtrahend) -> ILoaderClass:
"""
Overview:
Create a minus loader.
Arguments:
- subtrahend (:obj:`Any`): The subtrahend.
"""
return _math_binary(lambda x, y: x - y, subtrahend)
def minus_with(minuend) -> ILoaderClass:
"""
Overview:
Create a minus loader.
Arguments:
- minuend (:obj:`Any`): The minuend.
"""
return _math_binary(lambda x, y: y - x, minuend)
def multi(multiplier) -> ILoaderClass:
"""
Overview:
Create a multi loader.
Arguments:
- multiplier (:obj:`Any`): The multiplier.
"""
return _math_binary(lambda x, y: x * y, multiplier)
def divide(divisor) -> ILoaderClass:
"""
Overview:
Create a divide loader.
Arguments:
- divisor (:obj:`Any`): The divisor.
"""
return _math_binary(lambda x, y: x / y, divisor)
def divide_with(dividend) -> ILoaderClass:
"""
Overview:
Create a divide loader.
Arguments:
- dividend (:obj:`Any`): The dividend.
"""
return _math_binary(lambda x, y: y / x, dividend)
def power(index) -> ILoaderClass:
"""
Overview:
Create a power loader.
Arguments:
- index (:obj:`Any`): The index.
"""
return _math_binary(lambda x, y: x ** y, index)
def power_with(base) -> ILoaderClass:
"""
Overview:
Create a power loader.
Arguments:
- base (:obj:`Any`): The base.
"""
return _math_binary(lambda x, y: y ** x, base)
def msum(*items) -> ILoaderClass:
"""
Overview:
Create a sum loader.
Arguments:
- items (:obj:`tuple`): The items.
"""
def _load(value):
return sum([item(value) for item in items])
return Loader(_load)
def mmulti(*items) -> ILoaderClass:
"""
Overview:
Create a multi loader.
Arguments:
- items (:obj:`tuple`): The items.
"""
def _load(value):
_result = 1
for item in items:
_result *= item(value)
return _result
return Loader(_load)
_COMPARE_OPERATORS = {
'!=': operator.__ne__,
'==': operator.__eq__,
'<': operator.__lt__,
'<=': operator.__le__,
'>': operator.__gt__,
'>=': operator.__ge__,
}
def _msinglecmp(first, op, second) -> ILoaderClass:
"""
Overview:
Create a single compare loader.
Arguments:
- first (:obj:`Any`): The first item.
- op (:obj:`str`): The operator.
- second (:obj:`Any`): The second item.
"""
first = Loader(first)
second = Loader(second)
if op in _COMPARE_OPERATORS.keys():
return Loader(
(
lambda x: _COMPARE_OPERATORS[op](first(x), second(x)), lambda x: ValueError(
'comparison failed for {first} {op} {second}'.format(
first=repr(first(x)),
second=repr(second(x)),
op=op,
)
)
)
)
else:
raise KeyError('Invalid compare operator - {op}.'.format(op=repr(op)))
def mcmp(first, *items) -> ILoaderClass:
"""
Overview:
Create a multi compare loader.
Arguments:
- first (:obj:`Any`): The first item.
- items (:obj:`tuple`): The items.
"""
if len(items) % 2 == 1:
raise ValueError('Count of items should be odd number but {number} found.'.format(number=len(items) + 1))
ops, items = items[0::2], items[1::2]
_result = keep()
for first, op, second in zip((first, ) + items[:-1], ops, items):
_result &= _msinglecmp(first, op, second)
return check_only(_result)