Source code for itertree.itree_mathsets

# -*- coding: utf-8 -*-
"""
This code is taken from the itertree package:
https://pypi.org/project/itertree/
GIT Home:
https://github.com/BR1py/itertree
The documentation can be found here:
https://itertree.readthedocs.io/en/latest/index.html

The code is published under MIT license incl. human protect patch:

The MIT License (MIT) incl. human protect patch
Copyright © 2022 <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the “Software”), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Human protect patch:
The program and its derivative work will neither be modified or executed to harm any human being nor through
inaction permit any human being to be harmed.

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information see: https://en.wikipedia.org/wiki/MIT_License


This part of code contains
helper classes used in DataTree object
"""

from __future__ import absolute_import

import abc
import math
import re
from decimal import Decimal
from operator import le, lt, gt, ge
from itertree.itree_helpers import *

UNION = 0
INTERSECT = 1

_VAR_START_CHARACTERS = {i for i in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'}
_VAR_CHARACTERS = {i for i in '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'}
_NUMBER_START_CHARACTERS = {i for i in '-+1234567890i'}
_CALL = 0
_FORMAT = 1
_OLD = 2


def _get_formatter_type(formatter):
    """
    helper function to identify the formatter type

    In case of str type formatters the formatter stripped from spaces is given back
    :type formatter: Union[str,Callable]
    :param formatter: to be identified object
    :rtype: tuple
    :return: formatter, formatter-types
    """
    if callable(formatter):
        if type(formatter(0)) is not str:
            raise TypeError('Given formatter must deliver a str')
        return formatter, _CALL
    elif type(formatter) is str:
        formatter = formatter.strip(' ')
        if formatter[0] == '{':
            return formatter, _FORMAT
        return formatter, _OLD
    raise TypeError('Given formatter is no string or Callable')


[docs]class mSetItem(): """ item object to used in the different mSet objects Depending on the object it is a "normal" item (`mSetRoster`) or it is the lower or upper limit of the `mSetInterval` object. In first case complemented items will be ignored. The object contains a formatting option to define how the string representation of the item should look like. Especially integer formatting is used to to create representations of the other base like hex or octal. :type value: object :param value: numerical value to be stored in object or definition str the user can give also a variable-name here :type complement: bool :param complement: complement type item (only required for interval limits :type fromatter: Union[Callable,str] :param formatter: explicit formatter user can give as formatter 1. formatter method (callable) 2. escape string for `format()` method 3 escape string for classical "%" replacement """ def __init__(self, value, complement=False, formatter=str): """ :type value: object :param value: numerical value to be stored in object or definition str the user can give also a variable-name here :type complement: bool :param complement: complement type item (only required for interval limits :type fromatter: Union[Callable,str] :param formatter: explicit formatter user can give as formatter 1. formatter method (callable) 2. escape string for `format()` method 3 escape string for classical "%" replacement """ self._complement = bool(complement) self._is_var = False formatter = _get_formatter_type(formatter) if type(value) is str: value = value.strip(' ') if value[0] in _VAR_START_CHARACTERS and value != 'inf': var_chars = _VAR_CHARACTERS for c in value: if c not in var_chars: raise TypeError('Given variable name contains invalid characters (%s)' % repr(c)) self._is_var = True elif value[0] in _NUMBER_START_CHARACTERS: if value[0] == '-': factor = -1 value = value[1:].strip(' ') elif value[0] == '+': factor = 1 value = value[1:].strip(' ') else: factor = 1 # we try to extract a number from the input string if value == 'inf': value = float('inf') else: i = value.find('.') try: if i == -1: if value[0] == '0': if value[1].lower() == 'x': value = int(value, 16) formatter = hex, 0 elif value[1].lower() == 'o': value = int(value, 8) formatter = oct, 0 elif value[1].lower() == 'b': value = int(value, 2) formatter = bin, 0 else: value = int(value) else: value = int(value) else: if formatter[0] is str: i2 = -1 if 'e' in value: i2 = value.find('e') elif 'E' in value: i2 = value.find('E') if i2 != -1: formatter = '{:%i.%ie}' % (i, i2 - i - 1), _FORMAT else: formatter = '{:%i.%if}' % (i, len(value) - i - 1), _FORMAT value = float(value) except: raise TypeError( 'Given str value cannot be interpreted as an %s object' % self.__class__.__name__) value = value * factor else: raise TypeError('Given str value cannot be interpreted as an %s object' % self.__class__.__name__) else: # we check if the given value is a number by casting it to float try: float(value) except TypeError: raise TypeError('Give value is not a numeric type that is supported by this class') self._formatter = formatter self._value = value @property def is_mSetItem(self): """ Property used for identification of the object :rtype: bool :return: True """ return True @property def is_complement(self): """ property tells if the object is complement :rtype: bool :return: True/False """ return self._complement @property def value(self): """ property contains the value of the item :return: value of the object """ return self._value @property def formatter(self): """ property delivers the formatter of the object :rtype: Union[Callable,str] :return: formatter object (Callable or string) """ return self._formatter[0] @property def formatter_type(self): """ property delivers the formatter type integer constant of the object formatter :rtype: int :return: formatter type integer ( `_CALL`~0;,`_FORMAT`~1; ,`_OLD`~2 ) """ return self._formatter[1] @property def is_var(self): """ property returns if the value is a variable name or a normal numerical value :rtype: bool :return: True - is variable; False - normal numerical value """ return self._is_var
[docs] def get_init_args(self, full=False): """ get the initial arguments for instance the object :type full: bool :param full: do not shorten even that we have default values :rtype: tuple :return: tuple of init arguments """ if full or self._formatter[0] is not str: return (self._value, self._complement, self._formatter[0]) elif self._complement: return (self._value, self._complement) else: return (self._value,)
[docs] def math_repr(self, formatter=None): """ delivers the formatted value :rtype formatter: Union[Callable,str,None][ :param formatter: optionally an explicit formatter can be given :rtype: str :return: Formatted value stored in the `mSetItem`-object """ if self.is_var: return self._value if formatter: formatter = _get_formatter_type(formatter) else: formatter = self._formatter if formatter[1] == _CALL: return formatter[0](self._value) elif formatter[1] == _FORMAT: return formatter[0].format(self._value) else: return formatter[0] % (self._value)
def __gt__(self, other): """ greater than given other object :param other: object self is compared to :return: True self is larger or False other is larger """ if self.is_var or hasattr(other, 'is_var') and other.is_var: return None if hasattr(other, 'value'): return self._value > other.value else: return self._value > other def __lt__(self, other): """ smaller than given other object :param other: object self is compared to :return: True self is smaller or False other is smaller """ if self.is_var or hasattr(other, 'is_var') and other.is_var: return None if hasattr(other, 'value'): return self._value < other.value else: return self._value < other def __ge__(self, other): """ greater or equal than given other object :param other: object self is compared to :return: True self is larger or equal; or False other is larger """ if self.is_var or hasattr(other, 'is_var') and other.is_var: return None if hasattr(other, 'value'): return self._value >= other.value else: return self._value >= other def __le__(self, other): """ smaller or equal than given other object :param other: object self is compared to :return: True self is smaller or equal; or False other is smaller """ if self.is_var or hasattr(other, 'is_var') and other.is_var: return None if hasattr(other, 'value'): return self._value <= other.value else: return self._value <= other def __eq__(self, other): """ other is same object from the content? :param other: other object :return: True ~ other has equal content; False ~ other has different content """ if hasattr(other, 'is_mSetItem'): if (self.is_complement + other.is_complement) & 0b1: # XOR for complements return self._value != other.value else: return self._value == other.value else: if self._complement: return self._value != other else: return self._value == other def __float__(self): """ converts (casts) stored value in a float In case conversion is not possible (variable) float('NaN') is returned :rtype: float :return: float conversion result """ if self.is_var: return float('NaN') return float(self._value) def __repr__(self): """ string representation :rtype: str :return: representation string of the object """ out = [self.__class__.__name__, '('] value = self._value if value == float('inf') or value == float('-inf') or self.is_var: v = "'%s'" % str(value) else: v = repr(self._value) out.append(v) out.append(', ') args = self.get_init_args()[1:] for arg in args: out.append(repr(arg)) out.append(', ') out[-1] = ')' return ''.join(out) def __str__(self): """ string representation using math_repr() as parameter :rtype: str :return: representation string """ out = [self.__class__.__name__, '('] if self.is_var: out.append(repr(self._value)) else: out.append(self.math_repr()) out.append(', ') args = self.get_init_args()[1:] for arg in args: out.append(repr(arg)) out.append(', ') out[-1] = ')' return ''.join(out) def __hash__(self): if self._complement: return hash((self._value, self._complement)) return hash(self._value)
class _mSetBase(abc.ABC): """ super class for all mSet objects handles two parameters :param vars: variable names set :param complement: complement flag """ def __init__(self, vars, complement=False): """ handles two parameters :param vars: variable names set :param complement: complement flag """ self._complement = bool(complement) self._vars = vars def __len__(self): """ The cardinality is somehow the size of the set it delivers how many items the set contains. The result is not in all cases correct furthermore it is just an estimation! In many cases in float intervals the user will find infinite as the result of the operation. :return: number of items integer or float('inf') for infinite results """ return self.cardinality() def __sub__(self, other): # intersection return mSetCombine(self, other, False) def __add__(self, other): # union return mSetCombine(self, other, True) @property def is_mSet(self): """ used for object identification :rtype: bool :return: True """ return True @property def is_complement(self): """ is this a complemented set? :rtype: bool :return: True is complemented; False is normal """ return self._complement def switch_complement(self): """ switch the complement flag :rtype: bool :return: current complement flag (after switching) """ self._complement = complement = not self._complement return complement @property def has_vars(self): """ do we have variables in the object? :rtype: bool :return: True we have variables; False we don't have variables """ return bool(len(self._vars)) @property def vars(self): """ deliver set of variable names a :rtype: set :return: set of variable names """ return self._vars @abc.abstractmethod def cardinality(self): """ The cardinality is somehow the size of the set it delivers how many items the set contains. The result is not in all cases correct furthermore it is just an estimation! In many cases in float intervals the user will find infinite as the result of the operation. :return: number of items integer or float('inf') for infinite results """ pass @abc.abstractmethod def is_empty_set(self): """ For some set definition no matching item can be found! Then the set is equal to the empty set and this property will deliver True :rtype: bool :return: True is empty set; False set contains items """ pass @abc.abstractmethod def is_empty_set_complement(self): """ For some set definition no matching item can be found! Then the set is equal to the empty set. Here we do not check the set itself, we check if the complement of the set is empty. Somehow if this property delivers True we can say that the set is the universal set. :rtype: bool :return: True is full/universal set (complement is empty); False is not full/universal set (complement is not empty) """ pass @abc.abstractmethod def __contains__(self, value, vars_dict=None): """ checks if given value is inside the set (This is the main function of the whole object!) :type value: Union[iterable,Numeric,tuple] :param value: value to be checked if it is in. Because "in" supports no parameters the vars_dict can be given as a tuple in the form: (value, vars_dict) in this_object :type vars_dict: dict :param vars_dict: optional replacement dict for variable items :rtype: bool :return: True is in; False is not in the set """ pass @abc.abstractmethod def __repr__(self): """ create representation string :rtype: str :return: representation string """ pass @abc.abstractmethod def __str__(self): """ create representation string us math_repr as parameter :rtype: str :return:representation str """ pass def __call__(self, value, vars_dict=None): """ Use method like call to check if the given value is in this object. Same as __contains__() :param value: value to be checked :param vars_dict: variables replacement dict :return: True/False """ return self.__contains__(value, vars_dict) @abc.abstractmethod def iter_in(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object the result is a iterable over the single results :param value: to be checked iterable value (single item check) :param vars_dict: variable replacement dict :return: iterable True/False """ pass @abc.abstractmethod def filter(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object or not in case it is in the item will be delivered back if not it is skipped :param value: iterable value which items will be checked :param vars_dict: variable replacement dict :return: iterable of matching items """ pass @abc.abstractmethod def get_init_args(self, full=False): """ delivers tuple of all initial arguments given to instance the mSet object :param full: True all arguments given also defaults :return: tuple of initial arguments """ pass @abc.abstractmethod def math_repr(self): """ mathematical representation of the object (we try to match as good as possible to the mathematical standards here but we avoid exotic characters! :return: mathematical representation string """ pass
[docs]class mSetInterval(_mSetBase): """ Mathematical interval set object. Here the user can define a mathematical interval with closed or open boarders. For more details related to mathematical intervals you may have a look here: https://en.wikipedia.org/wiki/Interval_(mathematics) """ _REGEX_MATH_REP_DEF = r'(\!\(|\!\[|\(|\[)(.+)+(,|\.\.)(.+)+(\)\'|\]\'|\)|\])' _REGEX_MATH_REP = re.compile(_REGEX_MATH_REP_DEF) # group 1: ( or { or !( or !{ # group 2: lower value # group 3 , or .. # group 4 upper value # group 5 ) or } or )' or }' # pre-condition: all spaces are eliminated! _REGEX_BUILDER_DEF = r'(\!\{|\{)([a-zA-Z])(\|)' \ r'(((?P<lv1>[a-zA-Z\d\.\_\+\-]*)' \ r'(((?P<lo12>\<\=|\>\=|\<|\>)\2(?P<uo12>\<\=|\<|\>\=|\>))|' \ r'((?<=\|)\2(?P<uo1>\!\=\=|\!\=|\=\=|\=|\<\=|\<|\>\=|\>|E|e))|' \ r'((?P<lo1>\!\=\=|\!\=|\=\=|\<\=|\>\=|\=|\<|\>)\2(?=\,)))' \ r'(?P<uv1>[a-zA-Z\d\.\_\+\-]*)\,)?' \ r'((?P<lv2>[a-zA-Z\d\.\_\+\-]*)' \ r'(((?P<lo22>\<\=|\>\=|\<|\>)\2(?P<uo22>\<\=|\<|\>\=|\>))|' \ r'((?<=(\,|\|))\2(?P<uo2>\!\=\=|\!\=|\=\=|\=|\<\=|\<|\>\=|\>|E|e))|' \ r'((?P<lo2>\!\=\=|\!\=|\=\=|\<\=|\>\=|\=|\<|\>)\2(?=\})))' \ r'(?P<uv2>[a-zA-Z\d\.\_\+\-]*)))' \ r'(\}\'|\})' _REGEX_BUILDER = re.compile(_REGEX_BUILDER_DEF) # group 1 { or !{ # group2 varname # group 3 | # group4 full content # we allow here maximum 2 more sub-groups (comma separated) # first part exists only in case a comma separator is used # group 6 lower_value 1 (2 operator definition) # group 9 lower_operator 1 (2 operator definition) # group 10 upper_operator 1 (2 operator definition) # group 12 upper_operator 1 (1 operator) # group 14 lower_operator 1 (1 operator definition) # group 15 upper_value 1 (or set type) # These groups should always exists somehow: # group 17 lower_value 2 # group 20 lower value operator 2 (2 operator definition) # group 26 lower value operator 2 (1 operator definition variable left) # group 21 upper value operator 2 (2 operator definition) (can be also e or E for element) # group 24 upper value operator 2 (1 operator) (can be also e or E for element) # group 27 upper_value 2 (or set type) # number of sub-sub-groups depend on the content # last group contains the ending } or }' def __init__(self, *definition, lower=None, upper=None, int_only=False, complement=False): """ :param definition: pointer to all unnamed parameters given If only one is given and the one is a string the parsers try to extract/construct the interval object from the given mathematical interval definition given. We support direct definitions like "[1,2]" (closed) or "(1,2)" (open) but also most type builder definitions are supported like {x| 2<=x<1} ~ (1,2] To define integer based intervals the user must use ".." instead of "," as seperator: [1,2] ~ float and [1..2] ~ int For builder definitions the user can use numerical set domains to define the number type: {x|x e Z, 2<=x<1} ~[(1..2] ~ int definition The numerical set domain can be in most cases extended by 0+- to limit the valid range: e.g: Z,Z+,Z0+,Z-,Z0- Finally we have also a simplified builder definition available with the fixed variable name x for floats and n for integers: 2>=x>1 ~ (1,2] or 2>=n>1 ~ (1..2] If one limit is not given it will be set to the maximum (lower ~ float('-inf') and upper ~ float('inf') :param lower: lower limit value (Give tuple (value,True) for open definitions or mSetItem(value,complement=True, formatter='%e') (with formatter example) :param upper: upper limit value (Give tuple (value,True) for open definitions or mSetItem(value,complement=True, formatter='%e') (formatter example) :param int_only: Flag for integer only intervals True ~ int; Flase ~ float :param complement: Interval complement """ self._int_only = bool(int_only) s = len(definition) paras = None if s > 0: if lower is not None: raise TypeError('Unnamed and named parameters are in conflict') t = type(definition[0]) if t is mSetItem: lower = definition[0] elif definition[0] is None: lower = mSetItem(float('-inf')) elif t is tuple: lower = mSetItem(*definition[0]) elif t is str: def_str = definition[0].strip(' ') if def_str[0] == '!': pre_complement_found = True def_str = def_str[1:].strip(' ') else: pre_complement_found = False if def_str[0] == '[' or def_str[0] == '(': paras = self._parse_math_definition_str(def_str, pre_complement_found) elif def_str[0] == '{': paras = self._parse_builder_definition_str(def_str, pre_complement_found) else: try: lower = mSetItem(definition[0]) except: if 'x' in def_str: # simplified definition using "x" given? try: paras = self._parse_builder_definition_str('{x|' + def_str + '}', pre_complement_found) except: raise TypeError('Issue with first parameter given, argument parsing failed') elif 'n' in def_str: # simplified definition using "x" given? try: paras = self._parse_builder_definition_str('{n|neZ,' + def_str + '}', pre_complement_found) except: raise TypeError('Issue with first parameter given, argument parsing failed') else: raise else: lower = mSetItem(definition[0]) if paras: if upper is not None: raise TypeError('Unnamed and named parameters are in conflict') lower = paras[0] upper = paras[1] if s > 1: self._int_only = definition[1] or self._int_only self._int_only = paras[2] or self._int_only if s > 2: complement = definition[2] or complement complement = paras[3] or complement if s > 3: raise TypeError('Too many unnamed parameters given') else: if s > 1: if upper is not None: raise TypeError('Unnamed and named parameters are in conflict') t = type(definition[1]) if t is mSetItem: upper = definition[1] elif definition[1] is None: upper = mSetItem(float('inf')) elif t is tuple: upper = mSetItem(*definition[1]) else: upper = mSetItem(definition[1]) if s > 2: self._int_only = definition[2] or self._int_only if s > 3: complement = definition[3] or complement if s > 4: raise TypeError('Too many unnamed parameters given') t = type(lower) if t is not mSetItem: if lower is None: lower = mSetItem(float('-inf')) elif t is tuple: lower = mSetItem(*lower) else: lower = mSetItem(lower) t = type(upper) if t is not mSetItem: if upper is None: upper = mSetItem(float('inf')) elif t is tuple: upper = mSetItem(*upper) else: upper = mSetItem(upper) vars = set() if lower.is_var: vars.add(lower._value) if upper.is_var: vars.add(upper._value) super().__init__(vars, complement) if not vars: if lower > upper: # switch order lower, upper = upper, lower self._lower_op = lt if lower.is_complement else le self._upper_op = gt if upper.is_complement else ge if upper.value == lower.value and (self._lower_op == le or self._upper_op == ge): self._lower_op = le self._upper_op = ge self._upper = upper self._lower = lower @property def is_lower_closed(self): """ do we have a closed lower limit "(" :return: True is closed, False is open """ return not self._lower.is_complement @property def is_lower_open(self): """ do we have a open lower limit "(" :return: True is open, False is closed """ return self._lower.is_complement @property def lower_value(self): """ Property delivers the lower limit value :return: value of the lower limit """ return self._lower.value @property def is_upper_closed(self): """ do we have a closed upper limit "(" :return: True is closed, False is open """ return not self._upper.is_complement @property def is_upper_open(self): """ do we have a open upper limit "(" :return: True is open, False is closed """ return self._upper.is_complement @property def upper_value(self): """ Property delivers the upper limit value :return: value of the upper limit """ return self._upper.value @property def is_int_only(self): """ Is this an integer number only interval? :return: """ return self._int_only def _cardinality_without_complement(self): """ Helper function to calculate the cardinality (size/number of items) :return: integer or float('inf') """ if self._vars: if self._upper.value == self._lower.value: if self._lower._complement and self._upper._complement: return 0 else: return 1 else: return float('inf') elif self._int_only: if self._lower.is_complement: lower = self._lower.value + 1 else: lower = self._lower.value if self._upper.is_complement: upper = self._upper.value - 1 else: upper = self._upper.value diff = (upper - lower) + 1 if diff < 0: return 0 elif diff == 0 and not (self._upper.is_complement and self._lower.is_complement): return 1 return diff else: if self._upper.value == self._lower.value: if self._lower._complement and self._upper._complement: return 0 else: return 1 else: return float('inf') @property def cardinality(self): """ The cardinality is somehow the size of the set it delivers how many items the set contains. The result is not in all cases correct furthermore it is just an estimation! In many cases in float intervals the user will find infinite as the result of the operation. :return: number of items integer or float('inf') for infinite results """ c = self._cardinality_without_complement() if self._complement: if c == float('inf') and self._lower.value == float('-inf') and self._upper.value == float('inf'): return 0 else: return float('inf') else: return c @property def is_empty_set(self): """ Is the interval same as an empty set (no item inside) :return: True is empty; False is not empty """ if self.is_complement: return False return self._cardinality_without_complement() == 0 @property def is_empty_set_complement(self): """ Is the complement interval same as an empty set (no item inside). If this is the case the set is the universal set (any item inside). But this is a relative definition to the "universe". E.g. strings will never be found inside a numerical set they are not in the "universe". :return: True complement is empty; False complement is not empty """ if not self.is_complement: return False return self._cardinality_without_complement() == 0 def _get_lower_upper(self, vars_dict=None): """ helper method to replace variable boarders with real values :param vars_dict: dict containing the replacement values for the variables :return: replaced values of the limits """ lower = self._lower.value upper = self._upper.value if vars_dict is None or not self._vars: return lower, upper for key in self._vars: if key in vars_dict: if key == lower: lower = vars_dict[key] if key == upper: upper = vars_dict[key] return lower, upper def _parse_math_definition_str(self, definition, pre_complement=False): """ parser for math_rper strings given to instance the object :param definition: definition string :param pre_complement: complement flag that is pre parsed :return: parameter set for the instance """ def_str = definition.replace(' ', '') match = self._REGEX_MATH_REP.match(def_str) if match: if match.end() != len(def_str): raise TypeError('After parsing additional characters found in the definition, somethings wrong') groups = match.groups() # how many main groups we have? try: lower = mSetItem(groups[1], groups[0] == '(') if lower.is_var: if groups[1] not in definition: raise TypeError('Spaces found in variable name (lower), this is not supported') except: raise TypeError('Something is wrong with the lower value, not interpretable') try: upper = mSetItem(groups[3], groups[4][0] == ')') if upper.is_var: if groups[3] not in definition: raise TypeError('Spaces found in variable name (upper), this is not supported') except: raise TypeError('Something is wrong with the upper value, not interpretable') is_int = groups[2] == '..' complement = (pre_complement + int(groups[4][-1] == "'")) & 0b1 return lower, upper, is_int, complement else: raise TypeError('Parsing error given interval definition') def _get_set_data(self, set_def_str): """ helper function for numerical domians :param set_def_str: definition string for the numerical domain :return: tuple with (integer True/False,lower_limit,upper_limit) """ set_def_str = set_def_str.replace('Integer', 'Z').replace('integer', 'Z').replace('int', 'Z') set_def_str = set_def_str.replace('Float', 'R').replace('float', 'R').replace('Double', 'R').replace('double', 'R') if set_def_str in {'bool', 'boolean', 'Boolean', 'N01', 'N10'}: return True, 0, 1 s = len(set_def_str) if s == 0: raise TypeError('Missing numerical set definition') if set_def_str[0] == 'N': if s == 1: return True, 0, float('inf') elif s == 2: if set_def_str[1] == '+': return True, 1, float('inf') elif set_def_str[1] == '0': return True, 0, float('inf') elif s == 3: if set_def_str[1] == '+': if set_def_str[2] == '0': return True, 0, float('inf') elif set_def_str[1] == '0': if set_def_str[2] == '+': return True, 0, float('inf') else: # the rest has same limits! is_int = False if set_def_str[0] == 'Z': is_int = True if s == 1: return is_int, float('-inf'), float('inf') elif s == 2: if set_def_str[1] == '+': return is_int, (0, True), float('inf') elif set_def_str[1] == '0': return is_int, float('-inf'), float('inf') elif set_def_str[1] == '-': return is_int, float('-inf'), (0, True) elif s == 3: if set_def_str[1] == '+': if set_def_str[2] == '0': return is_int, 0, float('inf') # We don't support R+- user should use x!=1 instead elif set_def_str[1] == '-': if set_def_str[2] == '0': return is_int, float('-inf'), 0 elif set_def_str[1] == '0': if set_def_str[2] == '-': return is_int, float('-inf'), 0 elif set_def_str[2] == '+': return is_int, 0, float('inf') raise TypeError('Unsupported set definition %s' % (repr(set_def_str))) def _replace_lower_limit(self, old, new_limit, is_equal): """ helper function to replaced lower limits with later parsed limit? :param old: old value :param new_limit: new_value :param is_equal: is equal already detected (limits possibilities) :return: mSetItem of the current lower limit """ if old is None: return mSetItem(new_limit) if is_equal: raise TypeError('Definition mixes a equal limits with range limits on the lower side') if old.has_vars: raise TypeError('Definition mixes a variable limit with a fixed limit on the lower side') t = type(new_limit) if t is tuple: # open boarder! if old < new_limit[0]: return mSetItem(new_limit) elif t is mSetItem: if new_limit.has_vars: raise TypeError('Definition mixes a variable limit with a fixed limit on the lower side') if new_limit.is_complement: if old < new_limit: return mSetItem(new_limit) elif old <= new_limit: return mSetItem(new_limit) elif old <= new_limit: return mSetItem(new_limit) return old def _replace_upper_limit(self, old, new_limit, is_equal): """ helper function to replaced upper limits with later parsed limit? :param old: old value :param new_limit: new_value :param is_equal: is equal already detected (limits possibilities) :return: mSetItem of the current lower limit """ if old is None: if type(new_limit) is tuple: return mSetItem(*new_limit) return mSetItem(new_limit) if is_equal: raise TypeError('Definition mixes a equal limits with range limits on the upper side') if old.is_var: raise TypeError('Definition mixes a variable limit with a fixed limit on the upper side') t = type(new_limit) if t is tuple: # open boarder! if old > new_limit[0]: return mSetItem(new_limit) elif t is mSetItem: if new_limit.is_var: raise TypeError('Definition mixes a variable limit with a fixed limit on the upper side') if new_limit.is_complement: if old > new_limit: return mSetItem(new_limit) elif old >= new_limit: return mSetItem(new_limit) elif old >= new_limit: return mSetItem(new_limit) return old def _parse_builder_definition_str(self, definition, pre_complement=False): """ Parser and mapper for interval definitions in builder style The parser is very complex and the mapping of the results to the parameters is even more complex. For details see: https://en.wikipedia.org/wiki/Interval_(mathematics) https://en.wikipedia.org/wiki/Set-builder_notation Our parser has the following general limits: 1. Only one "|" is supported 2. left of the "|" the user must place the notification variable (e.g. x) 3. just one unique notification variable is supported 4. right of the "|" the user can place one or two statements which must be separated by a comma "," 5. A numerical domain can be given via "e" or "E" which stands for "element of" 6. Equal statements {x|x=1} cannot be combined with other statements but the complement is supported in the operator too ("!=") 7. Complement can be given as leading "!" or as post "'" (must be escaped in python string definitions) 8. Multiple complements wil neutralize each other finally a even number of complements will lead into a not complemented interval In general the following numerical sets are supported as domains: * N -> [0..inf] natural numbers incl. zero * N+ -> [1..inf] natural numbers without zero * N0+ -> [0..inf] natural numbers incl. zero (explicit) * Z -> [-inf..inf] integers (following names can be used as replacement (only without extensions): int,integer,Integer) * Z+ -> [1..inf] * Z0+ -> [0..inf] * Z- -> [-inf..-1] * Z0- -> [-inf..0] * Q -> [-inf..inf] rational numbers and real numbers are internally handled as the same (floats) (Following replacements accepted: R (with extensions), float, Float, double, Double) * Q+ -> (0)..inf] * Q0+ -> [0..inf] * Q- -> [-inf..0) * Q0- -> [-inf..0] Additionally we support: * bool or Boolean [0..1] but it's very highly recommended not to mix such a definition with other statements in this case the user should just place an x on the right hand side. As explained we support a logical subset of possibilities but we did not extend the already very complex parser to cover more corner cases. The user should try to find the easiest logical notification. E.g: "{x| x e N0+- , x>10}" will raise a TypeError because of N0+- (what should be the minus in this case?) is not supported "{x| x e N0+ , x>10}" -> (10..inf] will work but the interpretation will take a lot of time Recommended would be : {x| x e Z, x> 10} -> (10..inf] The set N makes only sense for definitions targeting in the other direction: {x| x e N+, x><10} -> (0..10) We give here some examples which should be logically clear and simple formed: * {b| b e bool} -> [0..1] * {n| n e N , n<256 } -> [0..256) * {z| -128<=z<128, z e Z } -> [-128..128) * {x| -70000.555<=x<=,80000.555} -> [-70000.555,80000.555] * {x| x=0}} -> [0,0] * {x| x!=0}} -> ![0,0] Less good but still possible definitions are: * {x| -70000.555<=x ,80000.555>=x} -> [-70000.555,80000.555] it's always better to use statements like 1<=x<=2 * !{x| x!=0}}' -> ![0,0] (triple complement) * !{x| x!=0}} -> [0,0] (dual complement) * {n| n e Z0+ , n>256 } -> (256)..inf] Z0+ is not required just put Z * {x| x>10 , x>256 } -> (256,inf] avoid duplicated limits in same direction And this will crash * {x| x==10 , x>256 } -> TypeError (equal mixed with other operation) * {x| x==10 , x e N } -> TypeError (equal mixed with other operation) we deny this because there are to many cornercases that must be covered! * {n| n e Z0+- , n>256 } -> TypeError (Z0+- not supported) * {x| x==10 , x e N } -> TypeError (equal mixed with other operation) :param definition: definition string :param pre_complement: is pre complement already detected (leading "!" :return: New parameter set for mSetInterval """ def_str = definition.replace(' ', '') match = self._REGEX_BUILDER.match(def_str) if match: if match.end() != len(def_str): raise TypeError('After parsing additional characters found in the definition, somethings wrong') groups = match.groupdict() if groups['lv1'] or groups['uv1']: # two main groups upper = None lower = None is_int = False is_equal = False lo = groups['lo1'] uo = groups['uo1'] lo2 = groups['lo12'] uo2 = groups['uo12'] if lo2: lo = lo2 if uo2: uo = uo2 if lo is None: pass elif lo == '>=': # upper limit upper = self._replace_upper_limit(upper, mSetItem(groups['lv1']), is_equal) elif lo == '>': # upper limit upper = self._replace_upper_limit(upper, mSetItem(groups['lv1'], True), is_equal) elif lo == '<=': lower = self._replace_lower_limit(lower, mSetItem(groups['lv1']), is_equal) elif lo == '<': lower = self._replace_lower_limit(lower, mSetItem(groups['lv1'], True), is_equal) elif lo[-1] == '=': raise TypeError('Equal operator definitions cannot be mixed with other operators') elif lo == 'e' or lo == 'E': numerical_set = groups['uv1'] is_int, lower_limit, upper_limit = self._get_set_data(numerical_set) # here we have a very special case of equal definition afterwards which would lead into issues: # therefore we will return already here in this case lo = groups['lo2'] uo = groups['uo2'] lo2 = groups['lo22'] uo2 = groups['uo22'] if lo2: lo = lo2 if uo2: uo = uo2 if lo in {'!=', '!==', '==', '='}: raise TypeError('Equal operator definitions cannot be mixed with other operators') if uo in {'!=', '!==', '==', '='}: raise TypeError('Equal operator definitions cannot be mixed with other operators') lower = self._replace_upper_limit(lower, lower_limit, False) # ignore equal flag upper = self._replace_upper_limit(upper, upper_limit, False) else: raise TypeError('Parsed operator %s invalid' % repr(lo2)) if uo is None: pass elif uo == '>=': # lower limit lower = self._replace_lower_limit(lower, mSetItem(groups['uv1']), is_equal) elif uo == '>': # lower limit lower = self._replace_lower_limit(lower, mSetItem(groups['uv1'], True), is_equal) elif uo == '<=': upper = self._replace_upper_limit(upper, mSetItem(groups['uv1']), is_equal) elif uo == '<': upper = self._replace_upper_limit(upper, mSetItem(groups['uv1'], True), is_equal) elif uo[-1] == '=': raise TypeError('Equal operator definitions cannot be mixed with other operators') elif uo == 'e' or uo == 'E': numerical_set = groups['uv1'] is_int, lower_limit, upper_limit = self._get_set_data(numerical_set) # here we have a very special case of equal definition afterwards which would lead into issues: # therefore we will return already here in this case lo = groups['lo2'] uo = groups['uo2'] lo2 = groups['lo22'] uo2 = groups['uo22'] if lo2: lo = lo2 if uo2: uo = uo2 if lo in {'!=', '!==', '==', '='}: raise TypeError('Equal operator definitions cannot be mixed with other operators') if uo in {'!=', '!==', '==', '='}: raise TypeError('Equal operator definitions cannot be mixed with other operators') lower = self._replace_upper_limit(lower, lower_limit, False) # ignore equal flag upper = self._replace_upper_limit(upper, upper_limit, False) else: raise TypeError('Parsed operator %s invalid' % repr(uo2)) else: # one main group only upper = None lower = None is_int = False is_equal = False lo = groups['lo2'] uo = groups['uo2'] lo2 = groups['lo22'] uo2 = groups['uo22'] if lo2: lo = lo2 if uo2: uo = uo2 if lo is None: pass elif lo == '>=': # upper limit upper = self._replace_upper_limit(upper, mSetItem(groups['lv2']), is_equal) elif lo == '>': # upper limit upper = self._replace_upper_limit(upper, mSetItem(groups['lv2'], True), is_equal) elif lo == '<=': lower = self._replace_lower_limit(lower, mSetItem(groups['lv2']), is_equal) elif lo == '<': lower = self._replace_lower_limit(lower, mSetItem(groups['lv2'], True), is_equal) elif lo == '==' or lo == '=': if lower or upper: raise TypeError('Equal operator definitions cannot be mixed with other operators') is_equal = True lower = mSetItem(groups['lv2']) upper = lower elif lo == '!==' or lo == '!=': is_equal = True if lower or upper: raise TypeError('Equal operator definitions cannot be mixed with other operators') lower = mSetItem(groups['lv2']) upper = lower pre_complement = (pre_complement + 1) & 0b1 else: raise TypeError('Parsed operator %s invalid' % repr(lo2)) if uo is None: pass elif uo == '>=': # lower limit lower = self._replace_lower_limit(lower, mSetItem(groups['uv2']), is_equal) elif uo == '>': # lower limit lower = self._replace_lower_limit(lower, mSetItem(groups['uv2'], True), is_equal) elif uo == '<=': upper = self._replace_upper_limit(upper, mSetItem(groups['uv2']), is_equal) elif uo == '<': upper = self._replace_upper_limit(upper, mSetItem(groups['uv2'], True), is_equal) elif uo == '==' or uo == '=': if lower or upper: raise TypeError('Equal operator definitions cannot be mixed with other operators') is_equal = True lower = mSetItem(groups['uv2']) upper = lower elif uo == '!==' or uo == '!=': if lower or upper: raise TypeError('Equal operator definitions cannot be mixed with other operators') is_equal = True lower = mSetItem(groups['uv2']) upper = lower pre_complement = (pre_complement + 1) & 0b1 elif uo == 'e' or uo == 'E': numerical_set = groups['uv2'] is_int, lower_limit, upper_limit = self._get_set_data(numerical_set) lower = self._replace_upper_limit(lower, lower_limit, is_equal) # ignore equal flag upper = self._replace_upper_limit(upper, upper_limit, is_equal) else: raise TypeError('Parsed operator %s invalid' % repr(uo2)) complement = (pre_complement + int(match.groups()[27][-1] == "'")) & 0b1 return lower, upper, is_int, complement else: raise TypeError('Parsing error given interval definition') def __contains__(self, value, vars_dict=None, _limits=None): """ checks if given value is inside the set (This is the main function of the whole object!) :type value: Union[iterable,Numeric,tuple] :param value: value to be checked if it is in. Because "in" supports no parameters the vars_dict can be given as a tuple in the form: (value, vars_dict) in this_object :type vars_dict: dict :param vars_dict: optional replacement dict for variable items :rtype: bool :return: True is in; False is not in the set """ if not vars_dict and type(value) is tuple and len(value) == 2 and type(value[1]) is dict: value, vars_dict = value if _limits: lower, upper = _limits else: lower, upper = _limits = self._get_lower_upper(vars_dict) if hasattr(value, 'capitalize'): if self._complement: return True else: return False if hasattr(value, '__iter__') or hasattr(value, '__next__'): # iterable return all(self.__contains__(v, _limits=_limits) for v in value) try: if self.is_int_only: if int(value) != value: result = False else: result = self._lower_op(lower, value) and self._upper_op(upper, value) else: result = self._lower_op(lower, value) and self._upper_op(upper, value) except TypeError: if self.has_vars: if vars_dict is None: raise TypeError('Set contains variables but no vars_dict given') not_set_vars = [] for var in self.vars: if var not in vars_dict: not_set_vars.append(var) raise TypeError('Not all required variables set for comparison (%s) ' % repr(not_set_vars)[1:-1]) else: raise if self._complement: return not result else: return result def __repr__(self): """ string representation :rtype: str :return: representation string of the object """ out = [self.__class__.__name__, '(', repr(self._lower), ', ', repr(self._upper)] if self.is_complement: if self.is_int_only: out.append(', True') else: out.append(', False') out.append(', True)') else: if self.is_int_only: out.append(', True)') else: out.append(')') return ''.join(out) def __str__(self): """ string representation using math_repr() as parameter :rtype: str :return: representation string """ return ''.join([self.__class__.__name__, '(', repr(self.math_repr()), ')']) def __eq__(self, other): if type(other) is not mSetInterval: return False return self.get_init_args() == other.get_init_args() def __iter__(self): if not self.is_complement and self.is_int_only and not self.has_vars \ and self._upper.value != float('inf') and self._lower.value != float('-inf'): if self._lower.is_complement: start = self._lower.value + 1 else: start = self._lower.value if self._upper.is_complement: end = self._upper.value - 1 else: end = self._upper.value if start is None or end is None: return if start > end: return elif start == end: yield start else: for i in range(start, end + 1): yield i return
[docs] def iter_in(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object the result is a iterable over the single results :param value: to be checked iterable value (single item check) :param vars_dict: variable replacement dict :return: iterable True/False """ limits = self._get_lower_upper(vars_dict) for v in value: yield self.__contains__(v, _limits=limits)
[docs] def filter(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object or not in case it is in the item will be delivered back if not it is skipped :param value: iterable value which items will be checked :param vars_dict: variable replacement dict :return: iterable of matching items """ limits = self._get_lower_upper(vars_dict) for v in value: if self.__contains__(v, _limits=limits): yield v
[docs] def get_init_args(self, full=False): """ delivers tuple of all initial arguments given to instance the mSet object :param full: True all arguments given also defaults :return: tuple of initial arguments """ if full or self.is_complement: return (self._lower, self._upper, self.is_int_only, True) elif self.is_int_only: return (self._lower, self._upper, True) else: return (self._lower, self._upper)
[docs] def math_repr(self, formatters=None): """ mathematical representation of the object (we try to match as good as possible to the mathematical standards here but we avoid exotic characters! :return: mathematical representation string """ if self.is_complement: out = ['!'] else: out = [] lower = self._lower if lower.is_complement: out.append('(') else: out.append('[') if formatters: out.append(lower.math_repr(formatters[0])) else: out.append(lower.math_repr()) if self.is_int_only: out.append('..') else: out.append(',') upper = self._upper if formatters: out.append(upper.math_repr(formatters[1])) else: out.append(upper.math_repr()) if upper.is_complement: out.append(')') else: out.append(']') return ''.join(out)
[docs]class mSetRoster(_mSetBase): def __init__(self, *definition, items=None, complement=False): item_set = set() if items is not None: for item in items: t = type(item) if t is mSetItem: pass elif t is tuple(): item = mSetItem(*item) else: item = mSetItem(item) if not item.is_complement: item_set.add(item) s = len(definition) if s != 0: if type(definition[-1]) is bool: complement = (int(complement) + int(definition[-1])) & 0b1 definition = definition[:-1] s = len(definition) if s == 0: pass elif s == 1: not_parsed = True if type(definition[0]) is str: def_str = definition[0].replace(' ', '') try: new_items, n_complement = self._math_def_parser(def_str) item_set.update(new_items) complement = (int(complement) + int(n_complement)) & 0b1 not_parsed = False except: pass if not_parsed: t = type(definition[0]) if t is mSetItem: item = definition[0] elif t is tuple: item = mSetItem(*definition[0]) else: item = mSetItem(definition[0]) if not item.is_complement: item_set.add(item) else: for item in definition: t = type(item) if t is mSetItem: pass elif t is tuple(): item = mSetItem(*item) else: item = mSetItem(item) if not item.is_complement: item_set.add(item) t = type(definition[-1]) if t is mSetItem: item = definition[-1] elif t is tuple: item = mSetItem(*definition[-1]) elif t is bool: complement = (int(complement) + int(item)) & 0b1 item = None else: item = mSetItem(definition[-1]) if item is not None and not item.is_complement: item_set.add(item) vars = set() for item in item_set: if item.is_var: vars.add(item.value) super().__init__(vars, complement) self._item_set = item_set def _math_def_parser(self, def_str): def_str = def_str.replace(' ', '') complement = False if def_str[0] == '!': complement = not complement def_str = def_str[1:] if def_str[-1] == "'": complement = not complement def_str = def_str[:-1] if def_str[0] != '{' and def_str[-1] != '}': raise TypeError('Given RosterSet definition is invalid (no open/close) bracket pair found') items = {mSetItem(item) for item in def_str[1:-1].split(',') if item != ''} return items, complement @property def cardinality(self): """ The cardinality is somehow the size of the set it delivers how many items the set contains. The result is not in all cases correct furthermore it is just an estimation! In many cases in float intervals the user will find infinite as the result of the operation. :return: number of items integer or float('inf') for infinite results """ if self.is_complement: return float('inf') return len(self._item_set) @property def is_empty_set(self): """ For some set definition no matching item can be found! Then the set is equal to the empty set and this property will deliver True :rtype: bool :return: True is empty set; False set contains items """ return not bool(self.cardinality) @property def is_empty_set_complement(self): """ Is the complement interval same as an empty set (no item inside). If this is the case the set is the universal set (any item inside). But this is a relative definition to the "universe". E.g. strings will never be found inside a numerical set they are not in the "universe". :return: True complement is empty; False complement is not empty """ if self.is_complement: return not bool(len(self._item_set)) else: return False
[docs] def items(self): return self._item_set.__iter__()
def __contains__(self, value, vars_dict=None, _replacement_vars=set()): """ checks if given value is inside the set (This is the main function of the whole object!) :type value: Union[iterable,Numeric,tuple] :param value: value to be checked if it is in. Because "in" supports no parameters the vars_dict can be given as a tuple in the form: (value, vars_dict) in this_object :type vars_dict: dict :param vars_dict: optional replacement dict for variable items :rtype: bool :return: True is in; False is not in the set """ if not vars_dict and type(value) is tuple and len(value) == 2 and type(value[1]) is dict: value, vars_dict = value if not _replacement_vars and vars_dict is not None and self._vars: _replacement_vars = set() for var in self._vars: if var in vars_dict: _replacement_vars.add(mSetItem(vars_dict[var])) if not hasattr(value, 'capitalize') and hasattr(value, '__iter__') or hasattr(value, '__next__'): # iterable return all(self.__contains__(v, _replacement_vars=_replacement_vars) for v in value) result = value in self._item_set or value in _replacement_vars if self._complement: return not result else: return result def __repr__(self): """ string representation :rtype: str :return: representation string of the object """ if len(self._item_set): out = [self.__class__.__name__, '(', repr(self._item_set)[1:-1]] else: out = [self.__class__.__name__, '('] if self.is_complement: out.append(', True)') else: out.append(')') return ''.join(out) def __str__(self): """ string representation using math_repr() as parameter :rtype: str :return: representation string """ return ''.join([self.__class__.__name__, '(', self.math_repr(), ')']) def __iter__(self): return self._item_set.__iter__() def __eq__(self, other): if type(other) is mSetRoster: return self._item_set == other._item_set and self._complement == other._complement else: if self.is_complement: if hasattr(other, 'is_complement'): if not other.is_complement: return False else: return False try: for item, other_item in zip(self._item_set, other.items): if item != other_item: return False return True except: return False
[docs] def iter_in(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object the result is a iterable over the single results :param value: to be checked iterable value (single item check) :param vars_dict: variable replacement dict :return: iterable True/False """ _replacement_vars = set() if vars_dict is not None and self._vars: for var in self._vars: if var in vars_dict: _replacement_vars.add(mSetItem(vars_dict[var])) for v in value: yield self.__contains__(v, _replacement_vars=_replacement_vars)
[docs] def filter(self, value, vars_dict=None): _replacement_vars = set() if vars_dict is not None and self._vars: for var in self._vars: if var in vars_dict: _replacement_vars.add(mSetItem(vars_dict[var])) for v in value: if self.__contains__(v, _replacement_vars=_replacement_vars): yield v
[docs] def get_init_args(self, full=False): """ delivers tuple of all initial arguments given to instance the mSet object :param full: True all arguments given also defaults :return: tuple of initial arguments """ if full or self.is_complement: return tuple(self._item_set) + (self.is_complement,) else: return tuple(self._item_set)
[docs] def math_repr(self, formatters=None): if self.is_complement: out = ['!{'] else: out = ['{'] if formatters is None: s = -1 else: s = len(formatters) for i, item in enumerate(self._item_set): if i < s: out.append(item.math_repr(formatters[i])) else: out.append(item.math_repr()) out.append(',') if out[-1] == ',': out[-1] = '}' else: out.append('}') return ''.join(out)
[docs]class mSetCombine(_mSetBase): """ class where the user can combine different sets to unions In this class the user can combine different types of sets (all objects with `__contains__()` and a length are allowed to be added. If the object is used to check if a value is in it is sufficient if the value is in one of the subsets to create a positive response for a match """ def __init__(self, *definition, is_union=True, complement=False): """ :param definition: pointer parameter containing all unnamed arguments we expect here somehow one item for each set that should be integrated into the union. :param complement: complement flag """ if len(definition) == 0: raise TypeError('Minimum one sub-item must be given!') if type(definition[-1]) is bool: is_union = definition[-1] definition = definition[:-1] if len(definition) == 0: raise TypeError('Minimum one sub-item must be given!') if type(definition[-1]) is bool: complement = (int(complement) + int(is_union)) & 0b1 is_union = definition[-1] definition = definition[:-1] s = len(definition) if s == 0: raise TypeError('Minimum one sub-item must be given!') elif s == 1 and type(definition[0]) is str: items, is_union, complement = self._parse_math_repr(definition[0], complement) else: items = definition vars = set() for item in items: if hasattr(item, 'vars'): vars.update(item.vars) self._is_union = is_union self._items = items super().__init__(vars, complement) def __eq__(self, other): if type(other) is not mSetCombine: return False try: for i, ii in zip(self.items(), other.items()): if i != ii: return False except: return False if self.is_complement != other.is_complement: return False if self.is_union != other.is_union: return False return True def _parse_math_repr(self, def_str, complement=False): """ parser for given math_repr to instance the object :param def_str: :return: """ items = [] vars = set() def_str = def_str.strip(' ') if def_str[0] == '!': complement = not complement def_str = def_str[1:].strip(' ') if def_str[-1] == "'": complement = not complement def_str = def_str[:-1].strip(' ') if def_str[0] == '(' and def_str[-1] == ')': try: def_str = def_str[1:-1].strip(' ') return self._parse_math_repr(def_str, complement) except: def_str = '(' + def_str + ')' is_union = True tmp = def_str.split(' u ') tmp2 = def_str.split(' n ') if len(tmp) > 1 and len(tmp2) > 1: raise TypeError('mSetCombines supports just one type of combinations " u "~unions or " n "~intersections ') items = [] if len(tmp) > 1: for sub_def in tmp: try: item = mSetInterval(sub_def) except TypeError: try: item = mSetRoster(sub_def) except TypeError: item = mSetCombine(sub_def) items.append(item) elif len(tmp2) > 1: is_union = False for sub_def in tmp2: try: item = mSetInterval(sub_def) except TypeError: try: item = mSetRoster(sub_def) except TypeError: item = mSetCombine(sub_def) items.append(item) else: # single item only try: item = mSetInterval(def_str) except TypeError: try: item = mSetRoster(def_str) except TypeError: item = mSetCombine(def_str) items.append(item) return items, is_union, complement @property def is_union(self): return self._is_union @property def is_intersection(self): return not self._is_union
[docs] def items(self): for item in self._items: yield item
def _get_cardinality_without_complement(self): """ Helper function to estimate the cardinality :return: """ if self._is_union: c = 0 for i in self._items: if hasattr(i, 'cardinality'): c = c + i.cardinality else: c = c + len(i) return c else: c = float('inf') for i in self._items: if hasattr(i, 'cardinality'): c2 = i.cardinality if c2 < c: c = c2 return c @property def cardinality(self): """ The cardinality is somehow the size of the set it delivers how many items the set contains. The result is not in all cases correct furthermore it is just an estimation! Especially in this case the cardinally is really an estimation only. It's not teh case that we check here for overlapping intervals which might reduce the cardinality. We create just an estimation based the cardinalities of the subitems In many cases in float intervals the user will find infinite as the result of the operation. :return: number of items integer or float('inf') for infinite results """ return self._get_cardinality_without_complement() @property def is_empty_set(self): """ For some set definition no matching item can be found! Then the set is equal to the empty set and this property will deliver True :rtype: bool :return: True is empty set; False set contains items """ return self.cardinality == 0 @property def is_empty_set_complement(self): """ Is the complement interval same as an empty set (no item inside). If this is the case the set is the universal set (any item inside). But this is a relative definition to the "universe". E.g. strings will never be found inside a numerical set they are not in the "universe". :return: True complement is empty; False complement is not empty """ if self.is_complement: return self._get_cardinality_without_complement() == 0 else: return False def __contains__(self, value, vars_dict=None): """ checks if given value is inside the set (This is the main function of the whole object!) :type value: Union[iterable,Numeric,tuple] :param value: value to be checked if it is in. Because "in" supports no parameters the vars_dict can be given as a tuple in the form: (value, vars_dict) in this_object :type vars_dict: dict :param vars_dict: optional replacement dict for variable items :rtype: bool :return: True is in; False is not in the set """ return_value = not self.is_complement if type(value) is tuple and len(value) == 2 and type(value[1]) is dict: value, vars_dict = value raise_exception = None if self._is_union: for item in self._items: if hasattr(item, 'is_mSet'): try: if item(value, vars_dict=vars_dict): return return_value except Exception as e: raise_exception = e else: if value in item: return return_value if raise_exception: raise raise_exception return not return_value else: return_value = self.is_complement for item in self._items: if hasattr(item, 'is_mSet'): try: if not item.__contains__(value, vars_dict=vars_dict): return return_value except Exception as e: raise_exception = e else: if value not in item: return return_value if raise_exception: raise raise_exception return not return_value def __repr__(self): """ string representation :rtype: str :return: representation string of the object """ out = [self.__class__.__name__, '('] for a in self.get_init_args(): out.append(repr(a)) out.append(', ') if out[-1] == ', ': out[-1] = ')' else: out.append(')') return ''.join(out) def __str__(self): """ string representation using math_repr() as parameter :rtype: str :return: representation string """ out = [self.__class__.__name__, '("', self.math_repr(), '")'] return ''.join(out)
[docs] def math_repr(self): """ mathematical representation of the object (we try to match as good as possible to the mathematical standards here but we avoid exotic characters! :return: mathematical representation string """ if self._complement: out = ['!('] else: out = [''] for item in self._items: if hasattr(item, 'math_repr'): out.append(item.math_repr()) else: out.append(repr(item)) if self._is_union: out.append(' u ') else: out.append(' n ') if len(self._items) > 0: out = out[:-1] if self._complement: out.append(')') return ''.join(out)
[docs] def iter_in(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object the result is a iterable over the single results :param value: to be checked iterable value (single item check) :param vars_dict: variable replacement dict :return: iterable True/False """ return [self.__contains__(v, vars_dict) for v in value]
[docs] def filter(self, value, vars_dict=None): """ For each item in the given iterable value we check if the item is in this mSet object or not in case it is in the item will be delivered back if not it is skipped :param value: iterable value which items will be checked :param vars_dict: variable replacement dict :return: iterable of matching items """ return [v for v in value if self.__contains__(v, vars_dict)]
[docs] def get_init_args(self, full=False): """ delivers tuple of all initial arguments given to instance the mSet object :param full: True all arguments given also defaults :return: tuple of initial arguments """ if len(self._items) == 1: items = (self._items[0], None) else: items = tuple(self._items) if self._complement or full: return items + (self._is_union, self._complement) if not self._is_union: return items + (self._is_union,) return items
def __simplify(self): """ This method tries to reduce the items and sets in the union """ # this can be improved del_full_items = [] for i, item in enumerate(self._items): t = type(item) if type(item) is mSetRoster: if not item.is_complement: del_items = set() for i3, v in enumerate(item.items()): for i2, item2 in enumerate(self._items): if i2 == i: continue if v in item2: del_items.add(v) break if len(del_items) > 0: if len(del_items) == (i3 + 1): del_full_items.append(item) else: for i3 in del_items: del item._items[i3] if len(item._items) == 0: del_full_items.append(item) if type(item) is mSetInterval: if not item.is_complement: for i2, item2 in enumerate(self._items): if i2 == i: continue if type(item2) is mSetInterval: if not item2.is_complement: if item.lower.value in item2 and item.lower.value in item2: del_full_items.append(item) break for item in del_full_items: self._items.pop(item)