Source code for metawards._interpret


from typing import Union as _Union

__all__ = ["Interpret"]


_Number = _Union[int, float]


def _clamp_range(val, minval, maxval):
    """Ensure that 'val' is clamped to between 'minval' and 'maxval'
       inclusive (if they have been set)
    """
    if minval is not None:
        if val < minval:
            return minval

    if maxval is not None:
        if val > maxval:
            return maxval

    return val


[docs]class Interpret: """This is a static class that provides some routines for interpreting values from inputs (normally strings). This is used heavily by code that reads values from the user or from files """
[docs] @staticmethod def string(s: any) -> str: """Interpret and return a string from 's'""" return str(s)
[docs] @staticmethod def random_integer(s: str = None, rng=None, minval: int = None, maxval: int = None) -> int: """Interpret a random integer from the passed string, specifying the random number generator to use, and optionally adding additional constraints on the minimum and maximum values """ if s is None: rmin = None rmax = None else: import re m = re.search(r"rand\(\s*(-?\d*)\s*\,?\s*(-?\d*)\s*\)", Interpret.string(s), re.IGNORECASE) if m is None: raise ValueError( f"Cannot interpret a random integer from {s}") try: rmin = int(m.groups()[0]) except Exception: rmin = None try: rmax = int(m.groups()[1]) except Exception: rmax = None if minval is not None: minval = int(minval) if rmin is None: rmin = minval else: if rmin < minval: rmin = minval if maxval is not None: maxval = int(maxval) if rmax is None: rmax = maxval else: if rmax > maxval: rmax = maxval if rmin is None: rmin = 1 if rmax is None: rmax = 2**32 - 1 if rmax < rmin: tmp = rmin rmin = rmax rmax = tmp from .utils._ran_binomial import ran_int # we want to be from 'min' to 'max' inclusive return ran_int(rng, rmin, rmax)
[docs] @staticmethod def random_number(s: str = None, rng=None, minval: float = None, maxval: float = None) -> float: """Interpret a random number (float) from the passed string, specifying the random number generator to use, and optionally adding additional constraints on the minimum and maximum values """ if s is None: rmin = None rmax = None else: import re m = re.search(r"rand\(\s*([-?\d\.]*)\s*\,?\s*([-?\d\.]*)\s*\)", Interpret.string(s), re.IGNORECASE) if m is None: raise ValueError( f"Cannot interpret a random integer from {s}") try: rmin = float(m.groups()[0]) except Exception: rmin = None try: rmax = float(m.groups()[1]) except Exception: rmax = None if minval is not None: minval = float(minval) if rmin is None: rmin = minval else: if rmin < minval: rmin = minval if maxval is not None: maxval = float(maxval) if rmax is None: rmax = maxval else: if rmax > maxval: rmax = maxval if rmin is None: rmin = 0.0 if rmax is None: rmax = rmin + 1.0 if rmax < rmin: tmp = rmin rmin = rmax rmax = tmp from .utils._ran_binomial import ran_uniform return rmin + ((rmax - rmin) * ran_uniform(rng))
[docs] @staticmethod def list(s: any): """Interpret and return a list from the passed string 's'""" try: import ast my_list = [] for val in ast.literal_eval(s): my_list.append(val) return my_list except Exception: raise ValueError(f"Cannot interpret a list from {s}")
[docs] @staticmethod def integer(s: any, rng=None, minval: int = None, maxval: int = None) -> int: """Interpret and return an integer from 's', using the passed random number generator if this is a request for a random integer, and within the specified bounds of 'minval' and 'maxval' if needed. This can interpret 's' as an expression, e.g. "6 / 3" etc. """ try: d = int(s) except Exception: d = None if d is not None: return _clamp_range(d, minval=minval, maxval=maxval) s = Interpret.string(s) try: d = Interpret.random_integer(s=s, rng=rng, minval=minval, maxval=maxval) return d except Exception: d = None try: from .utils._safe_eval import safe_eval_number d = int(safe_eval_number(s)) except Exception: d = None if d is not None: return _clamp_range(d, minval=minval, maxval=maxval) else: raise ValueError(f"Cannot interpret an integer from {s}")
[docs] @staticmethod def number(s: any, rng=None, minval: _Number = None, maxval: _Number = None) -> _Number: """Interpret and return a number (integer or float) using the passed random number generator if this is a request for a random number, and within the specified bound of 'minval' and 'maxval' is needed This can interpret 's' as an expression, e.g. "2.4 * 3.6" etc. """ try: d = float(s) if d.is_integer(): d = int(s) except Exception: d = None if d is not None: return _clamp_range(d, minval=minval, maxval=maxval) s = Interpret.string(s) try: d = Interpret.random_number(s=s, rng=rng, minval=minval, maxval=maxval) return d except Exception: d = None try: from .utils._safe_eval import safe_eval_number d = safe_eval_number(s) except Exception: d = None if d is not None: return _clamp_range(d, minval=minval, maxval=maxval) else: raise ValueError(f"Cannot interpret a number from {s}")
[docs] @staticmethod def boolean(s: any, rng=None) -> bool: """Interpret and return a boolean (True or False) using the passed random number generator if this is a request for a random boolean """ if s is None: return False elif isinstance(s, bool): return s else: s = Interpret.string(s) s = s.strip().lower() if s in ["true", "yes", "on"]: return True elif s in ["false", "no", "off"]: return False d = Interpret.integer(s, rng=rng, minval=0, maxval=1) if d == 0: return False else: return True
[docs] @staticmethod def date(s: any, allow_fuzzy: bool = True): """Return a Python datetime.date object from the passed 's', allowing fuzzy dates if 'allow_fuzzy' is true """ s = Interpret.string(s) try: from datetime import date d = date.fromisoformat(s) except Exception: d = None if d is None: try: from dateparser import parse d = parse(s).date() except Exception: d = None if d is None: raise ValueError(f"Could not interpret a date from '{s}'") return d
[docs] @staticmethod def day(s: any, rng=None, minval: int = None, maxval: int = None) -> int: """Return a day number (integer) from the passed 's'. This is a shorthand for 'integer', but may take on more meaning if the day needs to be more specialised """ return Interpret.integer(s=s, rng=rng, minval=minval, maxval=maxval)
[docs] @staticmethod def day_or_date(s: any, rng=None, minval: int = None, maxval: int = None, allow_fuzzy: bool = True): """Convenience function that matches a day or a date from the passed 's' """ # Try a simple integer first, to prevent date weirdness try: return int(s) except Exception: pass # Do date first so that we can parse POSIX dates try: return Interpret.date(s=s, allow_fuzzy=allow_fuzzy) except Exception: pass try: return Interpret.day(s=s, rng=rng, minval=minval, maxval=maxval) except Exception: pass raise ValueError(f"Cannot interpret a day or date from {s}")