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)