Module interval_util.interval

Expand source code
import math
import warnings
from numbers import Number
from collections.abc import Sequence
from .util import *


class Interval(Sequence):
    """
    Specifies an open, closed or mixed interval.
    """

    inf = math.inf

    @property
    def is_empty(self):
        return self.end == self.start and (self.end_open or self.start_open)

    @property
    def length(self):
        if self.is_empty:
            return 0.0
        return self.end - self.start

    @property
    def middle(self):
        return (self.end + self.start) / 2

    @property
    def is_negative_infinite(self):
        return self.start != self.end and math.isinf(self.start)

    @property
    def is_positive_infinite(self):
        return self.start != self.end and math.isinf(self.end)

    @property
    def is_infinite(self):
        return self.is_negative_infinite and self.is_positive_infinite

    @property
    def is_finite(self):
        return not self.is_negative_infinite and not self.is_positive_infinite

    @property
    def is_point(self):
        return not self.is_empty and self.start == self.end

    def __init__(self, start, end, start_open=False, end_open=False):
        if start is None and end is not None:
            start = -math.inf
        if end is None and start is not None:
            end = math.inf

        self.start = 0
        self.end = 0
        self.start_open = start_open
        self.end_open = end_open

        if end == start and (end_open or start_open):
            # Empty interval
            start = None
            end = None
        else:
            assert isinstance(self.start, Number)
            assert isinstance(self.end, Number)
            assert self.end >= self.start

        self.start = start
        self.end = end

        if self.start != self.end:
            if self.is_negative_infinite:
                self.start_open = True
            if self.is_positive_infinite:
                self.end_open = True

    def __bool__(self):
        return not self.is_empty

    def __len__(self):
        return 2 if not self.is_empty else 0

    def __getitem__(self, i):
        return self.start if i == 0 else self.end

    def __iter__(self):
        if self.is_empty:
            return iter([])
        return iter([self.start, self.end])

    def as_closed(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=False, end_open=False)

    def as_open(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=True, end_open=True)

    def as_closed_open(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=False, end_open=True)

    def as_open_closed(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=True, end_open=False)

    def map(self, f):
        return Interval(f(self.start), f(self.end), start_open=self.start_open, end_open=self.end_open)

    def round(self, method=round):
        if self.is_empty:
            return self
        start = method(self.start) if not self.is_negative_infinite else self.start
        end = method(self.end) if not self.is_positive_infinite else self.end
        return Interval(start, end, start_open=self.start_open, end_open=self.end_open)

    def contains(self, x, enforce_start=True, enforce_end=True):
        if x is None or self.is_empty:
            return False

        # Special infinite cases
        if self.is_negative_infinite and x == -math.inf:
            return True
        if self.is_positive_infinite and x == math.inf:
            return True

        if enforce_start:
            if x < self.start:
                return False
            elif self.start_open and x == self.start:
                return False
        if enforce_end:
            if x > self.end:
                return False
            elif self.start_open and x == self.start:
                return False
            elif self.end_open and x == self.end:
                return False
        return True

    def index_range(self, values, key=None):
        """
        Returns index range of values inside the interval
        as a tuple `(start index, end index)`, where
        the end index is exclusive.

        If no values are inside the interval, (0, 0)
        is returned.

        If values are not numbers, a `key` callable
        must be supplied, which returns a float.

        Assumes `values` are sorted.
        """
        if self.is_empty:
            return 0, 0
        values_len = len(values)
        if values_len == 0 or self.is_infinite:
            return 0, values_len

        i0 = max(0, bisect_objects(values, self.start, key=key) - 1)
        i1 = min(values_len, bisect_objects(values, self.end, key=key) + 1)

        if key is None:
            def key(x): return x

        while i0 != i1:
            x = key(values[i0])
            if x < self.start and not self.contains(x):
                i0 += 1
            else:
                break
        while i1 != 0 and i0 != i1:
            x = key(values[i1 - 1])
            if x > self.end and not self.contains(x):
                i1 -= 1
            else:
                break

        return i0, i1

    def filter(self, values, key=None):
        """
        Returns values inside the interval.

        If values are not numbers, a `key` callable
        must be supplied, which returns a float.

        Assumes `values` are sorted.
        """
        i0, i1 = self.index_range(values, key=key)
        return values[i0:i1]

    def is_superset_of(self, interval):
        if self.is_empty:
            return False

        u = self
        v = Interval.parse(interval)

        if v.is_empty:
            return True

        u0 = u.start
        u1 = u.end
        v0 = v.start
        v1 = v.end

        if u0 > v0:
            return False
        elif u0 == v0 and u.start_open and not v.start_open:
            return False

        if u1 < v1:
            return False
        elif u1 == v1 and u.end_open and not v.end_open:
            return False

        return True

    def is_subset_of(self, interval):
        return Interval.parse(interval).is_superset_of(self)

    def equals(self, interval):
        if interval is None:
            return False
        interval = Interval.parse(interval)
        if self.is_empty and interval.is_empty:
            return True
        return self.start == interval.start and self.end == interval.end and self.start_open == interval.start_open and self.end_open == interval.end_open

    def partition(self, xs, start_open=None, end_open=None):
        """
        `xs` are assumed to be in ascending order.
        """
        xs = list(filter(lambda x: self.contains(x), xs))
        len_xs = len(xs)
        if len_xs == 0:
            return [self]
        intervals = []
        i_last = len_xs - 1
        x_prev = None
        if end_open is not None:
            start_open = not end_open
        elif start_open is not None:
            end_open = not start_open
        else:
            start_open = False
            end_open = True
        for i in range(len_xs + 1):
            if i <= i_last:
                x = xs[i]
            if i == 0:
                d_start = self.start
                d_start_open = self.start_open
            else:
                d_start = x_prev
                d_start_open = start_open
            if i == len_xs:
                d_end = self.end
                d_end_open = self.end_open
            else:
                d_end = x
                d_end_open = end_open
            d = Interval(d_start, d_end, start_open=d_start_open,
                       end_open=d_end_open)
            if not d.is_empty:
                intervals.append(d)
            x_prev = x
        return intervals

    def copy(self):
        return Interval(self.start, self.end, start_open=self.start_open, end_open=self.end_open)

    def offset(self, offset):
        return Interval(self.start + offset, self.end + offset, start_open=self.start_open, end_open=self.end_open)
        
    def get_gte(self):
        """
        Return a interval from the start of this interval to positive infinity.
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(self.start, math.inf, start_open=self.start_open, end_open=False)

    def get_lte(self):
        """
        Return a interval from the negative infinity to the end of this interval.
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(-math.inf, self.end, start_open=False, end_open=self.end_open)

    def get_gt(self):
        """
        Return a interval from the end of this interval (non-inclusive) to positive infinity.
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(self.end, math.inf, start_open=not self.end_open, end_open=False)

    def get_lt(self):
        """
        Return a interval from negative infinity to the start of this interval (non-inclusive).
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(-math.inf, self.start, start_open=False, end_open=not self.start_open)
        
    def extended_to_positive_infinity(self):
        """
        Return a interval from the start of this interval to positive infinity.
        If this interval is empty, return an empty interval.

        *Deprecated, use get_gte() instead.*
        """
        warnings.warn('extended_to_positive_infinity() is deprecated, use get_gte() instead', DeprecationWarning)
        return self.get_gte()

    def extended_to_negative_infinity(self):
        """
        Return a interval from the negative infinity to the end of this interval.
        If this interval is empty, return an empty interval.

        *Deprecated, use get_lte() instead.*
        """
        warnings.warn('extended_to_negative_infinity() is deprecated, use get_lte() instead', DeprecationWarning)
        return self.get_lte()

    def rest_to_positive_infinity(self):
        """
        Return a interval from the end of this interval (non-inclusive) to positive infinity.
        If this interval is empty, return an empty interval.

        *Deprecated, use get_gt() instead.*
        """
        warnings.warn('rest_to_positive_infinity() is deprecated, use get_gt() instead', DeprecationWarning)
        return self.get_gt()

    def rest_to_negative_infinity(self):
        """
        Return a interval from negative infinity to the start of this interval (non-inclusive).
        If this interval is empty, return an empty interval.

        *Deprecated, use get_lt() instead.*
        """
        warnings.warn('rest_to_negative_infinity() is deprecated, use get_lt() instead', DeprecationWarning)
        return self.get_lt()

    def to_str(self, transformer=None, infinity_str=None, empty_str=None):
        if self.is_empty:
            return empty_str or '()'

        start_str = '(' if self.start_open else '['
        end_str = ')' if self.end_open else ']'

        start = self.start
        end = self.end

        if start == end:
            x = start
            if callable(transformer):
                x = transformer(x)
            return '{}{}{}'.format(start_str, x, end_str)

        if self.is_negative_infinite:
            start = '-' + (infinity_str or 'inf')
        elif callable(transformer):
            start = transformer(start)

        if self.is_positive_infinite:
            end = infinity_str or 'inf'
        elif callable(transformer):
            end = transformer(end)

        return '{}{}, {}{}'.format(start_str, start, end, end_str)

    def pad(self, *amount, start=None, end=None):
        default = amount[0] if len(amount) != 0 else None

        if self.is_negative_infinite:
            start = self.start
        else:
            start = self.start - (start or default or 0)

        if self.is_positive_infinite:
            end = self.end
        else:
            end = self.end + (end or default or 0)

        return Interval(start, end, start_open=self.start_open, end_open=self.end_open)

    @staticmethod
    def parse(d, default_inf=False):
        if d is None:
            if default_inf:
                return infinite
            else:
                raise Exception(f'Unable to parse interval: {d}')
        if type(d) == Interval or isinstance(d, Interval):
            return d
        elif isinstance(d, Number):
            return Interval.point(d)
        elif not bool(d):
            return empty
        elif isinstance(d, Sequence) and not isinstance(d, (str, bytes)):
            if len(d) == 2:
                return Interval(d[0], d[1])

        # TODO: parse interval strings such as: `[0, 4.5)`, `[0, +inf)`
        raise Exception(f'Unable to parse interval: {d}')

    @staticmethod
    def parse_many(ds):
        if type(ds) == Interval or isinstance(ds, Interval):
            return [ds]
        return list(map(Interval.parse, ds))

    @staticmethod
    def empty():
        return empty

    @staticmethod
    def infinite():
        return infinite

    @staticmethod
    def gt(x):
        return Interval(x, math.inf, start_open=True, end_open=True)

    @staticmethod
    def gte(x):
        return Interval(x, math.inf, start_open=False, end_open=True)

    @staticmethod
    def lt(x):
        return Interval(-math.inf, x, start_open=True, end_open=True)

    @staticmethod
    def lte(x):
        return Interval(-math.inf, x, start_open=True, end_open=False)

    @staticmethod
    def positive_infinite(x, open=False):
        """
        *Deprecated, use gt() or gte() instead.*
        """
        warnings.warn('positive_infinite() is deprecated, use gt() or gte() instead', DeprecationWarning)
        return Interval(x, math.inf, start_open=open, end_open=True)

    @staticmethod
    def negative_infinite(x, open=False):
        """
        *Deprecated, use t() or lte() instead.*
        """
        warnings.warn('negative_infinite() is deprecated, use lt() or lte() instead', DeprecationWarning)
        return Interval(-math.inf, x, start_open=True, end_open=open)

    @staticmethod
    def point(x):
        return Interval(x, x, start_open=False, end_open=False)

    @staticmethod
    def closed(start, end):
        return Interval(start, end, start_open=False, end_open=False)

    @staticmethod
    def open(start, end):
        return Interval(start, end, start_open=True, end_open=True)

    @staticmethod
    def closed_open(start, end):
        return Interval(start, end, start_open=False, end_open=True)

    @staticmethod
    def open_closed(start, end):
        return Interval(start, end, start_open=True, end_open=False)

    @staticmethod
    def union(intervals):
        intervals = Interval.parse_many(intervals)
        intervals = list(filter(lambda d: not d.is_empty, intervals))
        d_len = len(intervals)
        if d_len == 0:
            return empty
        elif d_len == 1:
            return intervals[0]
        start = min(intervals, key=lambda d: d.start).start
        end = max(intervals, key=lambda d: d.end).end
        # open if none are open
        start_open = not any(map(lambda d: not d.start_open, filter(
            lambda d: d.start == start, intervals)))
        end_open = not any(map(lambda d: not d.end_open,
                               filter(lambda d: d.end == end, intervals)))
        return Interval(start, end, start_open=start_open, end_open=end_open)

    @staticmethod
    def intersection(intervals):
        intervals = Interval.parse_many(intervals)
        for d in intervals:
            if d.is_empty:
                # intersection of any set with an empty set is an empty set
                return empty
        d_len = len(intervals)
        if d_len == 0:
            return empty
        elif d_len == 1:
            return intervals[0]
        start = max(intervals, key=lambda d: d.start).start
        end = min(intervals, key=lambda d: d.end).end
        if start > end:
            return empty
        # open if any is open
        start_open = any(map(lambda d: d.start_open, filter(
            lambda d: d.start == start, intervals)))
        end_open = any(map(lambda d: d.end_open, filter(
            lambda d: d.end == end, intervals)))
        return Interval(start, end, start_open=start_open, end_open=end_open)

    def intersects(self, interval):
        if self.is_empty:
            return False

        u = self
        v = Interval.parse(interval)

        if v.is_empty:
            return False

        u0 = u.start
        u1 = u.end
        v0 = v.start
        v1 = v.end

        if u0 <= v0:
            if u1 < v0:
                return False
            elif u1 == v0:
                return not u.end_open and not v.start_open
            else:
                return True

        if u0 >= v0:
            if v1 < u0:
                return False
            elif v1 == u0:
                return not v.end_open and not u.start_open
            else:
                return True

    def __repr__(self):
        try:
            return self.to_str()
        except Exception as e:
            return super().__repr__() + f'({e})'

    def __radd__(self, other):
        # sum() starts with 0 and then adds the first itme in the list to that.
        # So if the first item doesn’t know how to add itself to 0, Python fails.
        # But before it fails, Python tries to do a reversed add with the operators.
        return Interval.union([self, other])

    def __add__(self, other):
        return Interval.union([self, other])

    def __lt__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.end < other.start:
            return True
        elif self.end == other.start and (self.end_open or other.start_open):
            return True
        return False

    def __le__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.end < other.end:
            return True
        elif self.end == other.end and not (not self.end_open and other.end_open):
            return True
        return False

    def __eq__(self, other):
        return self.equals(other)

    def __ne__(self, other):
        return not self.equals(other)

    def __gt__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.start > other.end:
            return True
        elif self.end == other.start and (self.start_open or other.end_open):
            return True
        return False

    def __ge__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.start > other.start:
            return True
        elif self.start == other.start and not (not self.start_open and other.start_open):
            return True
        return False

    def __and__(self, other):
        return Interval.intersection([self, other])

    def __or__(self, other):
        return Interval.union([self, other])


empty = Interval(0, 0, start_open=True, end_open=True)
infinite = Interval(-math.inf, math.inf, start_open=True, end_open=True)

Classes

class Interval (start, end, start_open=False, end_open=False)

Specifies an open, closed or mixed interval.

Expand source code
class Interval(Sequence):
    """
    Specifies an open, closed or mixed interval.
    """

    inf = math.inf

    @property
    def is_empty(self):
        return self.end == self.start and (self.end_open or self.start_open)

    @property
    def length(self):
        if self.is_empty:
            return 0.0
        return self.end - self.start

    @property
    def middle(self):
        return (self.end + self.start) / 2

    @property
    def is_negative_infinite(self):
        return self.start != self.end and math.isinf(self.start)

    @property
    def is_positive_infinite(self):
        return self.start != self.end and math.isinf(self.end)

    @property
    def is_infinite(self):
        return self.is_negative_infinite and self.is_positive_infinite

    @property
    def is_finite(self):
        return not self.is_negative_infinite and not self.is_positive_infinite

    @property
    def is_point(self):
        return not self.is_empty and self.start == self.end

    def __init__(self, start, end, start_open=False, end_open=False):
        if start is None and end is not None:
            start = -math.inf
        if end is None and start is not None:
            end = math.inf

        self.start = 0
        self.end = 0
        self.start_open = start_open
        self.end_open = end_open

        if end == start and (end_open or start_open):
            # Empty interval
            start = None
            end = None
        else:
            assert isinstance(self.start, Number)
            assert isinstance(self.end, Number)
            assert self.end >= self.start

        self.start = start
        self.end = end

        if self.start != self.end:
            if self.is_negative_infinite:
                self.start_open = True
            if self.is_positive_infinite:
                self.end_open = True

    def __bool__(self):
        return not self.is_empty

    def __len__(self):
        return 2 if not self.is_empty else 0

    def __getitem__(self, i):
        return self.start if i == 0 else self.end

    def __iter__(self):
        if self.is_empty:
            return iter([])
        return iter([self.start, self.end])

    def as_closed(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=False, end_open=False)

    def as_open(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=True, end_open=True)

    def as_closed_open(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=False, end_open=True)

    def as_open_closed(self):
        if self.is_empty:
            return self
        return Interval(self.start, self.end, start_open=True, end_open=False)

    def map(self, f):
        return Interval(f(self.start), f(self.end), start_open=self.start_open, end_open=self.end_open)

    def round(self, method=round):
        if self.is_empty:
            return self
        start = method(self.start) if not self.is_negative_infinite else self.start
        end = method(self.end) if not self.is_positive_infinite else self.end
        return Interval(start, end, start_open=self.start_open, end_open=self.end_open)

    def contains(self, x, enforce_start=True, enforce_end=True):
        if x is None or self.is_empty:
            return False

        # Special infinite cases
        if self.is_negative_infinite and x == -math.inf:
            return True
        if self.is_positive_infinite and x == math.inf:
            return True

        if enforce_start:
            if x < self.start:
                return False
            elif self.start_open and x == self.start:
                return False
        if enforce_end:
            if x > self.end:
                return False
            elif self.start_open and x == self.start:
                return False
            elif self.end_open and x == self.end:
                return False
        return True

    def index_range(self, values, key=None):
        """
        Returns index range of values inside the interval
        as a tuple `(start index, end index)`, where
        the end index is exclusive.

        If no values are inside the interval, (0, 0)
        is returned.

        If values are not numbers, a `key` callable
        must be supplied, which returns a float.

        Assumes `values` are sorted.
        """
        if self.is_empty:
            return 0, 0
        values_len = len(values)
        if values_len == 0 or self.is_infinite:
            return 0, values_len

        i0 = max(0, bisect_objects(values, self.start, key=key) - 1)
        i1 = min(values_len, bisect_objects(values, self.end, key=key) + 1)

        if key is None:
            def key(x): return x

        while i0 != i1:
            x = key(values[i0])
            if x < self.start and not self.contains(x):
                i0 += 1
            else:
                break
        while i1 != 0 and i0 != i1:
            x = key(values[i1 - 1])
            if x > self.end and not self.contains(x):
                i1 -= 1
            else:
                break

        return i0, i1

    def filter(self, values, key=None):
        """
        Returns values inside the interval.

        If values are not numbers, a `key` callable
        must be supplied, which returns a float.

        Assumes `values` are sorted.
        """
        i0, i1 = self.index_range(values, key=key)
        return values[i0:i1]

    def is_superset_of(self, interval):
        if self.is_empty:
            return False

        u = self
        v = Interval.parse(interval)

        if v.is_empty:
            return True

        u0 = u.start
        u1 = u.end
        v0 = v.start
        v1 = v.end

        if u0 > v0:
            return False
        elif u0 == v0 and u.start_open and not v.start_open:
            return False

        if u1 < v1:
            return False
        elif u1 == v1 and u.end_open and not v.end_open:
            return False

        return True

    def is_subset_of(self, interval):
        return Interval.parse(interval).is_superset_of(self)

    def equals(self, interval):
        if interval is None:
            return False
        interval = Interval.parse(interval)
        if self.is_empty and interval.is_empty:
            return True
        return self.start == interval.start and self.end == interval.end and self.start_open == interval.start_open and self.end_open == interval.end_open

    def partition(self, xs, start_open=None, end_open=None):
        """
        `xs` are assumed to be in ascending order.
        """
        xs = list(filter(lambda x: self.contains(x), xs))
        len_xs = len(xs)
        if len_xs == 0:
            return [self]
        intervals = []
        i_last = len_xs - 1
        x_prev = None
        if end_open is not None:
            start_open = not end_open
        elif start_open is not None:
            end_open = not start_open
        else:
            start_open = False
            end_open = True
        for i in range(len_xs + 1):
            if i <= i_last:
                x = xs[i]
            if i == 0:
                d_start = self.start
                d_start_open = self.start_open
            else:
                d_start = x_prev
                d_start_open = start_open
            if i == len_xs:
                d_end = self.end
                d_end_open = self.end_open
            else:
                d_end = x
                d_end_open = end_open
            d = Interval(d_start, d_end, start_open=d_start_open,
                       end_open=d_end_open)
            if not d.is_empty:
                intervals.append(d)
            x_prev = x
        return intervals

    def copy(self):
        return Interval(self.start, self.end, start_open=self.start_open, end_open=self.end_open)

    def offset(self, offset):
        return Interval(self.start + offset, self.end + offset, start_open=self.start_open, end_open=self.end_open)
        
    def get_gte(self):
        """
        Return a interval from the start of this interval to positive infinity.
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(self.start, math.inf, start_open=self.start_open, end_open=False)

    def get_lte(self):
        """
        Return a interval from the negative infinity to the end of this interval.
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(-math.inf, self.end, start_open=False, end_open=self.end_open)

    def get_gt(self):
        """
        Return a interval from the end of this interval (non-inclusive) to positive infinity.
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(self.end, math.inf, start_open=not self.end_open, end_open=False)

    def get_lt(self):
        """
        Return a interval from negative infinity to the start of this interval (non-inclusive).
        If this interval is empty, return an empty interval.
        """
        if self.is_empty:
            return empty
        return Interval(-math.inf, self.start, start_open=False, end_open=not self.start_open)
        
    def extended_to_positive_infinity(self):
        """
        Return a interval from the start of this interval to positive infinity.
        If this interval is empty, return an empty interval.

        *Deprecated, use get_gte() instead.*
        """
        warnings.warn('extended_to_positive_infinity() is deprecated, use get_gte() instead', DeprecationWarning)
        return self.get_gte()

    def extended_to_negative_infinity(self):
        """
        Return a interval from the negative infinity to the end of this interval.
        If this interval is empty, return an empty interval.

        *Deprecated, use get_lte() instead.*
        """
        warnings.warn('extended_to_negative_infinity() is deprecated, use get_lte() instead', DeprecationWarning)
        return self.get_lte()

    def rest_to_positive_infinity(self):
        """
        Return a interval from the end of this interval (non-inclusive) to positive infinity.
        If this interval is empty, return an empty interval.

        *Deprecated, use get_gt() instead.*
        """
        warnings.warn('rest_to_positive_infinity() is deprecated, use get_gt() instead', DeprecationWarning)
        return self.get_gt()

    def rest_to_negative_infinity(self):
        """
        Return a interval from negative infinity to the start of this interval (non-inclusive).
        If this interval is empty, return an empty interval.

        *Deprecated, use get_lt() instead.*
        """
        warnings.warn('rest_to_negative_infinity() is deprecated, use get_lt() instead', DeprecationWarning)
        return self.get_lt()

    def to_str(self, transformer=None, infinity_str=None, empty_str=None):
        if self.is_empty:
            return empty_str or '()'

        start_str = '(' if self.start_open else '['
        end_str = ')' if self.end_open else ']'

        start = self.start
        end = self.end

        if start == end:
            x = start
            if callable(transformer):
                x = transformer(x)
            return '{}{}{}'.format(start_str, x, end_str)

        if self.is_negative_infinite:
            start = '-' + (infinity_str or 'inf')
        elif callable(transformer):
            start = transformer(start)

        if self.is_positive_infinite:
            end = infinity_str or 'inf'
        elif callable(transformer):
            end = transformer(end)

        return '{}{}, {}{}'.format(start_str, start, end, end_str)

    def pad(self, *amount, start=None, end=None):
        default = amount[0] if len(amount) != 0 else None

        if self.is_negative_infinite:
            start = self.start
        else:
            start = self.start - (start or default or 0)

        if self.is_positive_infinite:
            end = self.end
        else:
            end = self.end + (end or default or 0)

        return Interval(start, end, start_open=self.start_open, end_open=self.end_open)

    @staticmethod
    def parse(d, default_inf=False):
        if d is None:
            if default_inf:
                return infinite
            else:
                raise Exception(f'Unable to parse interval: {d}')
        if type(d) == Interval or isinstance(d, Interval):
            return d
        elif isinstance(d, Number):
            return Interval.point(d)
        elif not bool(d):
            return empty
        elif isinstance(d, Sequence) and not isinstance(d, (str, bytes)):
            if len(d) == 2:
                return Interval(d[0], d[1])

        # TODO: parse interval strings such as: `[0, 4.5)`, `[0, +inf)`
        raise Exception(f'Unable to parse interval: {d}')

    @staticmethod
    def parse_many(ds):
        if type(ds) == Interval or isinstance(ds, Interval):
            return [ds]
        return list(map(Interval.parse, ds))

    @staticmethod
    def empty():
        return empty

    @staticmethod
    def infinite():
        return infinite

    @staticmethod
    def gt(x):
        return Interval(x, math.inf, start_open=True, end_open=True)

    @staticmethod
    def gte(x):
        return Interval(x, math.inf, start_open=False, end_open=True)

    @staticmethod
    def lt(x):
        return Interval(-math.inf, x, start_open=True, end_open=True)

    @staticmethod
    def lte(x):
        return Interval(-math.inf, x, start_open=True, end_open=False)

    @staticmethod
    def positive_infinite(x, open=False):
        """
        *Deprecated, use gt() or gte() instead.*
        """
        warnings.warn('positive_infinite() is deprecated, use gt() or gte() instead', DeprecationWarning)
        return Interval(x, math.inf, start_open=open, end_open=True)

    @staticmethod
    def negative_infinite(x, open=False):
        """
        *Deprecated, use t() or lte() instead.*
        """
        warnings.warn('negative_infinite() is deprecated, use lt() or lte() instead', DeprecationWarning)
        return Interval(-math.inf, x, start_open=True, end_open=open)

    @staticmethod
    def point(x):
        return Interval(x, x, start_open=False, end_open=False)

    @staticmethod
    def closed(start, end):
        return Interval(start, end, start_open=False, end_open=False)

    @staticmethod
    def open(start, end):
        return Interval(start, end, start_open=True, end_open=True)

    @staticmethod
    def closed_open(start, end):
        return Interval(start, end, start_open=False, end_open=True)

    @staticmethod
    def open_closed(start, end):
        return Interval(start, end, start_open=True, end_open=False)

    @staticmethod
    def union(intervals):
        intervals = Interval.parse_many(intervals)
        intervals = list(filter(lambda d: not d.is_empty, intervals))
        d_len = len(intervals)
        if d_len == 0:
            return empty
        elif d_len == 1:
            return intervals[0]
        start = min(intervals, key=lambda d: d.start).start
        end = max(intervals, key=lambda d: d.end).end
        # open if none are open
        start_open = not any(map(lambda d: not d.start_open, filter(
            lambda d: d.start == start, intervals)))
        end_open = not any(map(lambda d: not d.end_open,
                               filter(lambda d: d.end == end, intervals)))
        return Interval(start, end, start_open=start_open, end_open=end_open)

    @staticmethod
    def intersection(intervals):
        intervals = Interval.parse_many(intervals)
        for d in intervals:
            if d.is_empty:
                # intersection of any set with an empty set is an empty set
                return empty
        d_len = len(intervals)
        if d_len == 0:
            return empty
        elif d_len == 1:
            return intervals[0]
        start = max(intervals, key=lambda d: d.start).start
        end = min(intervals, key=lambda d: d.end).end
        if start > end:
            return empty
        # open if any is open
        start_open = any(map(lambda d: d.start_open, filter(
            lambda d: d.start == start, intervals)))
        end_open = any(map(lambda d: d.end_open, filter(
            lambda d: d.end == end, intervals)))
        return Interval(start, end, start_open=start_open, end_open=end_open)

    def intersects(self, interval):
        if self.is_empty:
            return False

        u = self
        v = Interval.parse(interval)

        if v.is_empty:
            return False

        u0 = u.start
        u1 = u.end
        v0 = v.start
        v1 = v.end

        if u0 <= v0:
            if u1 < v0:
                return False
            elif u1 == v0:
                return not u.end_open and not v.start_open
            else:
                return True

        if u0 >= v0:
            if v1 < u0:
                return False
            elif v1 == u0:
                return not v.end_open and not u.start_open
            else:
                return True

    def __repr__(self):
        try:
            return self.to_str()
        except Exception as e:
            return super().__repr__() + f'({e})'

    def __radd__(self, other):
        # sum() starts with 0 and then adds the first itme in the list to that.
        # So if the first item doesn’t know how to add itself to 0, Python fails.
        # But before it fails, Python tries to do a reversed add with the operators.
        return Interval.union([self, other])

    def __add__(self, other):
        return Interval.union([self, other])

    def __lt__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.end < other.start:
            return True
        elif self.end == other.start and (self.end_open or other.start_open):
            return True
        return False

    def __le__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.end < other.end:
            return True
        elif self.end == other.end and not (not self.end_open and other.end_open):
            return True
        return False

    def __eq__(self, other):
        return self.equals(other)

    def __ne__(self, other):
        return not self.equals(other)

    def __gt__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.start > other.end:
            return True
        elif self.end == other.start and (self.start_open or other.end_open):
            return True
        return False

    def __ge__(self, other):
        if self.is_empty:
            return False
        other = Interval.parse(other)
        if other.is_empty:
            return True
        if self.start > other.start:
            return True
        elif self.start == other.start and not (not self.start_open and other.start_open):
            return True
        return False

    def __and__(self, other):
        return Interval.intersection([self, other])

    def __or__(self, other):
        return Interval.union([self, other])

Ancestors

  • collections.abc.Sequence
  • collections.abc.Reversible
  • collections.abc.Collection
  • collections.abc.Sized
  • collections.abc.Iterable
  • collections.abc.Container

Class variables

var inf

Static methods

def closed(start, end)
Expand source code
@staticmethod
def closed(start, end):
    return Interval(start, end, start_open=False, end_open=False)
def closed_open(start, end)
Expand source code
@staticmethod
def closed_open(start, end):
    return Interval(start, end, start_open=False, end_open=True)
def empty()
Expand source code
@staticmethod
def empty():
    return empty
def gt(x)
Expand source code
@staticmethod
def gt(x):
    return Interval(x, math.inf, start_open=True, end_open=True)
def gte(x)
Expand source code
@staticmethod
def gte(x):
    return Interval(x, math.inf, start_open=False, end_open=True)
def infinite()
Expand source code
@staticmethod
def infinite():
    return infinite
def intersection(intervals)
Expand source code
@staticmethod
def intersection(intervals):
    intervals = Interval.parse_many(intervals)
    for d in intervals:
        if d.is_empty:
            # intersection of any set with an empty set is an empty set
            return empty
    d_len = len(intervals)
    if d_len == 0:
        return empty
    elif d_len == 1:
        return intervals[0]
    start = max(intervals, key=lambda d: d.start).start
    end = min(intervals, key=lambda d: d.end).end
    if start > end:
        return empty
    # open if any is open
    start_open = any(map(lambda d: d.start_open, filter(
        lambda d: d.start == start, intervals)))
    end_open = any(map(lambda d: d.end_open, filter(
        lambda d: d.end == end, intervals)))
    return Interval(start, end, start_open=start_open, end_open=end_open)
def lt(x)
Expand source code
@staticmethod
def lt(x):
    return Interval(-math.inf, x, start_open=True, end_open=True)
def lte(x)
Expand source code
@staticmethod
def lte(x):
    return Interval(-math.inf, x, start_open=True, end_open=False)
def negative_infinite(x, open=False)

Deprecated, use t() or lte() instead.

Expand source code
@staticmethod
def negative_infinite(x, open=False):
    """
    *Deprecated, use t() or lte() instead.*
    """
    warnings.warn('negative_infinite() is deprecated, use lt() or lte() instead', DeprecationWarning)
    return Interval(-math.inf, x, start_open=True, end_open=open)
def open(start, end)
Expand source code
@staticmethod
def open(start, end):
    return Interval(start, end, start_open=True, end_open=True)
def open_closed(start, end)
Expand source code
@staticmethod
def open_closed(start, end):
    return Interval(start, end, start_open=True, end_open=False)
def parse(d, default_inf=False)
Expand source code
@staticmethod
def parse(d, default_inf=False):
    if d is None:
        if default_inf:
            return infinite
        else:
            raise Exception(f'Unable to parse interval: {d}')
    if type(d) == Interval or isinstance(d, Interval):
        return d
    elif isinstance(d, Number):
        return Interval.point(d)
    elif not bool(d):
        return empty
    elif isinstance(d, Sequence) and not isinstance(d, (str, bytes)):
        if len(d) == 2:
            return Interval(d[0], d[1])

    # TODO: parse interval strings such as: `[0, 4.5)`, `[0, +inf)`
    raise Exception(f'Unable to parse interval: {d}')
def parse_many(ds)
Expand source code
@staticmethod
def parse_many(ds):
    if type(ds) == Interval or isinstance(ds, Interval):
        return [ds]
    return list(map(Interval.parse, ds))
def point(x)
Expand source code
@staticmethod
def point(x):
    return Interval(x, x, start_open=False, end_open=False)
def positive_infinite(x, open=False)

Deprecated, use gt() or gte() instead.

Expand source code
@staticmethod
def positive_infinite(x, open=False):
    """
    *Deprecated, use gt() or gte() instead.*
    """
    warnings.warn('positive_infinite() is deprecated, use gt() or gte() instead', DeprecationWarning)
    return Interval(x, math.inf, start_open=open, end_open=True)
def union(intervals)
Expand source code
@staticmethod
def union(intervals):
    intervals = Interval.parse_many(intervals)
    intervals = list(filter(lambda d: not d.is_empty, intervals))
    d_len = len(intervals)
    if d_len == 0:
        return empty
    elif d_len == 1:
        return intervals[0]
    start = min(intervals, key=lambda d: d.start).start
    end = max(intervals, key=lambda d: d.end).end
    # open if none are open
    start_open = not any(map(lambda d: not d.start_open, filter(
        lambda d: d.start == start, intervals)))
    end_open = not any(map(lambda d: not d.end_open,
                           filter(lambda d: d.end == end, intervals)))
    return Interval(start, end, start_open=start_open, end_open=end_open)

Instance variables

var is_empty
Expand source code
@property
def is_empty(self):
    return self.end == self.start and (self.end_open or self.start_open)
var is_finite
Expand source code
@property
def is_finite(self):
    return not self.is_negative_infinite and not self.is_positive_infinite
var is_infinite
Expand source code
@property
def is_infinite(self):
    return self.is_negative_infinite and self.is_positive_infinite
var is_negative_infinite
Expand source code
@property
def is_negative_infinite(self):
    return self.start != self.end and math.isinf(self.start)
var is_point
Expand source code
@property
def is_point(self):
    return not self.is_empty and self.start == self.end
var is_positive_infinite
Expand source code
@property
def is_positive_infinite(self):
    return self.start != self.end and math.isinf(self.end)
var length
Expand source code
@property
def length(self):
    if self.is_empty:
        return 0.0
    return self.end - self.start
var middle
Expand source code
@property
def middle(self):
    return (self.end + self.start) / 2

Methods

def as_closed(self)
Expand source code
def as_closed(self):
    if self.is_empty:
        return self
    return Interval(self.start, self.end, start_open=False, end_open=False)
def as_closed_open(self)
Expand source code
def as_closed_open(self):
    if self.is_empty:
        return self
    return Interval(self.start, self.end, start_open=False, end_open=True)
def as_open(self)
Expand source code
def as_open(self):
    if self.is_empty:
        return self
    return Interval(self.start, self.end, start_open=True, end_open=True)
def as_open_closed(self)
Expand source code
def as_open_closed(self):
    if self.is_empty:
        return self
    return Interval(self.start, self.end, start_open=True, end_open=False)
def contains(self, x, enforce_start=True, enforce_end=True)
Expand source code
def contains(self, x, enforce_start=True, enforce_end=True):
    if x is None or self.is_empty:
        return False

    # Special infinite cases
    if self.is_negative_infinite and x == -math.inf:
        return True
    if self.is_positive_infinite and x == math.inf:
        return True

    if enforce_start:
        if x < self.start:
            return False
        elif self.start_open and x == self.start:
            return False
    if enforce_end:
        if x > self.end:
            return False
        elif self.start_open and x == self.start:
            return False
        elif self.end_open and x == self.end:
            return False
    return True
def copy(self)
Expand source code
def copy(self):
    return Interval(self.start, self.end, start_open=self.start_open, end_open=self.end_open)
def equals(self, interval)
Expand source code
def equals(self, interval):
    if interval is None:
        return False
    interval = Interval.parse(interval)
    if self.is_empty and interval.is_empty:
        return True
    return self.start == interval.start and self.end == interval.end and self.start_open == interval.start_open and self.end_open == interval.end_open
def extended_to_negative_infinity(self)

Return a interval from the negative infinity to the end of this interval. If this interval is empty, return an empty interval.

Deprecated, use get_lte() instead.

Expand source code
def extended_to_negative_infinity(self):
    """
    Return a interval from the negative infinity to the end of this interval.
    If this interval is empty, return an empty interval.

    *Deprecated, use get_lte() instead.*
    """
    warnings.warn('extended_to_negative_infinity() is deprecated, use get_lte() instead', DeprecationWarning)
    return self.get_lte()
def extended_to_positive_infinity(self)

Return a interval from the start of this interval to positive infinity. If this interval is empty, return an empty interval.

Deprecated, use get_gte() instead.

Expand source code
def extended_to_positive_infinity(self):
    """
    Return a interval from the start of this interval to positive infinity.
    If this interval is empty, return an empty interval.

    *Deprecated, use get_gte() instead.*
    """
    warnings.warn('extended_to_positive_infinity() is deprecated, use get_gte() instead', DeprecationWarning)
    return self.get_gte()
def filter(self, values, key=None)

Returns values inside the interval.

If values are not numbers, a key callable must be supplied, which returns a float.

Assumes values are sorted.

Expand source code
def filter(self, values, key=None):
    """
    Returns values inside the interval.

    If values are not numbers, a `key` callable
    must be supplied, which returns a float.

    Assumes `values` are sorted.
    """
    i0, i1 = self.index_range(values, key=key)
    return values[i0:i1]
def get_gt(self)

Return a interval from the end of this interval (non-inclusive) to positive infinity. If this interval is empty, return an empty interval.

Expand source code
def get_gt(self):
    """
    Return a interval from the end of this interval (non-inclusive) to positive infinity.
    If this interval is empty, return an empty interval.
    """
    if self.is_empty:
        return empty
    return Interval(self.end, math.inf, start_open=not self.end_open, end_open=False)
def get_gte(self)

Return a interval from the start of this interval to positive infinity. If this interval is empty, return an empty interval.

Expand source code
def get_gte(self):
    """
    Return a interval from the start of this interval to positive infinity.
    If this interval is empty, return an empty interval.
    """
    if self.is_empty:
        return empty
    return Interval(self.start, math.inf, start_open=self.start_open, end_open=False)
def get_lt(self)

Return a interval from negative infinity to the start of this interval (non-inclusive). If this interval is empty, return an empty interval.

Expand source code
def get_lt(self):
    """
    Return a interval from negative infinity to the start of this interval (non-inclusive).
    If this interval is empty, return an empty interval.
    """
    if self.is_empty:
        return empty
    return Interval(-math.inf, self.start, start_open=False, end_open=not self.start_open)
def get_lte(self)

Return a interval from the negative infinity to the end of this interval. If this interval is empty, return an empty interval.

Expand source code
def get_lte(self):
    """
    Return a interval from the negative infinity to the end of this interval.
    If this interval is empty, return an empty interval.
    """
    if self.is_empty:
        return empty
    return Interval(-math.inf, self.end, start_open=False, end_open=self.end_open)
def index_range(self, values, key=None)

Returns index range of values inside the interval as a tuple (start index, end index), where the end index is exclusive.

If no values are inside the interval, (0, 0) is returned.

If values are not numbers, a key callable must be supplied, which returns a float.

Assumes values are sorted.

Expand source code
def index_range(self, values, key=None):
    """
    Returns index range of values inside the interval
    as a tuple `(start index, end index)`, where
    the end index is exclusive.

    If no values are inside the interval, (0, 0)
    is returned.

    If values are not numbers, a `key` callable
    must be supplied, which returns a float.

    Assumes `values` are sorted.
    """
    if self.is_empty:
        return 0, 0
    values_len = len(values)
    if values_len == 0 or self.is_infinite:
        return 0, values_len

    i0 = max(0, bisect_objects(values, self.start, key=key) - 1)
    i1 = min(values_len, bisect_objects(values, self.end, key=key) + 1)

    if key is None:
        def key(x): return x

    while i0 != i1:
        x = key(values[i0])
        if x < self.start and not self.contains(x):
            i0 += 1
        else:
            break
    while i1 != 0 and i0 != i1:
        x = key(values[i1 - 1])
        if x > self.end and not self.contains(x):
            i1 -= 1
        else:
            break

    return i0, i1
def intersects(self, interval)
Expand source code
def intersects(self, interval):
    if self.is_empty:
        return False

    u = self
    v = Interval.parse(interval)

    if v.is_empty:
        return False

    u0 = u.start
    u1 = u.end
    v0 = v.start
    v1 = v.end

    if u0 <= v0:
        if u1 < v0:
            return False
        elif u1 == v0:
            return not u.end_open and not v.start_open
        else:
            return True

    if u0 >= v0:
        if v1 < u0:
            return False
        elif v1 == u0:
            return not v.end_open and not u.start_open
        else:
            return True
def is_subset_of(self, interval)
Expand source code
def is_subset_of(self, interval):
    return Interval.parse(interval).is_superset_of(self)
def is_superset_of(self, interval)
Expand source code
def is_superset_of(self, interval):
    if self.is_empty:
        return False

    u = self
    v = Interval.parse(interval)

    if v.is_empty:
        return True

    u0 = u.start
    u1 = u.end
    v0 = v.start
    v1 = v.end

    if u0 > v0:
        return False
    elif u0 == v0 and u.start_open and not v.start_open:
        return False

    if u1 < v1:
        return False
    elif u1 == v1 and u.end_open and not v.end_open:
        return False

    return True
def map(self, f)
Expand source code
def map(self, f):
    return Interval(f(self.start), f(self.end), start_open=self.start_open, end_open=self.end_open)
def offset(self, offset)
Expand source code
def offset(self, offset):
    return Interval(self.start + offset, self.end + offset, start_open=self.start_open, end_open=self.end_open)
def pad(self, *amount, start=None, end=None)
Expand source code
def pad(self, *amount, start=None, end=None):
    default = amount[0] if len(amount) != 0 else None

    if self.is_negative_infinite:
        start = self.start
    else:
        start = self.start - (start or default or 0)

    if self.is_positive_infinite:
        end = self.end
    else:
        end = self.end + (end or default or 0)

    return Interval(start, end, start_open=self.start_open, end_open=self.end_open)
def partition(self, xs, start_open=None, end_open=None)

xs are assumed to be in ascending order.

Expand source code
def partition(self, xs, start_open=None, end_open=None):
    """
    `xs` are assumed to be in ascending order.
    """
    xs = list(filter(lambda x: self.contains(x), xs))
    len_xs = len(xs)
    if len_xs == 0:
        return [self]
    intervals = []
    i_last = len_xs - 1
    x_prev = None
    if end_open is not None:
        start_open = not end_open
    elif start_open is not None:
        end_open = not start_open
    else:
        start_open = False
        end_open = True
    for i in range(len_xs + 1):
        if i <= i_last:
            x = xs[i]
        if i == 0:
            d_start = self.start
            d_start_open = self.start_open
        else:
            d_start = x_prev
            d_start_open = start_open
        if i == len_xs:
            d_end = self.end
            d_end_open = self.end_open
        else:
            d_end = x
            d_end_open = end_open
        d = Interval(d_start, d_end, start_open=d_start_open,
                   end_open=d_end_open)
        if not d.is_empty:
            intervals.append(d)
        x_prev = x
    return intervals
def rest_to_negative_infinity(self)

Return a interval from negative infinity to the start of this interval (non-inclusive). If this interval is empty, return an empty interval.

Deprecated, use get_lt() instead.

Expand source code
def rest_to_negative_infinity(self):
    """
    Return a interval from negative infinity to the start of this interval (non-inclusive).
    If this interval is empty, return an empty interval.

    *Deprecated, use get_lt() instead.*
    """
    warnings.warn('rest_to_negative_infinity() is deprecated, use get_lt() instead', DeprecationWarning)
    return self.get_lt()
def rest_to_positive_infinity(self)

Return a interval from the end of this interval (non-inclusive) to positive infinity. If this interval is empty, return an empty interval.

Deprecated, use get_gt() instead.

Expand source code
def rest_to_positive_infinity(self):
    """
    Return a interval from the end of this interval (non-inclusive) to positive infinity.
    If this interval is empty, return an empty interval.

    *Deprecated, use get_gt() instead.*
    """
    warnings.warn('rest_to_positive_infinity() is deprecated, use get_gt() instead', DeprecationWarning)
    return self.get_gt()
def round(self, method=<built-in function round>)
Expand source code
def round(self, method=round):
    if self.is_empty:
        return self
    start = method(self.start) if not self.is_negative_infinite else self.start
    end = method(self.end) if not self.is_positive_infinite else self.end
    return Interval(start, end, start_open=self.start_open, end_open=self.end_open)
def to_str(self, transformer=None, infinity_str=None, empty_str=None)
Expand source code
def to_str(self, transformer=None, infinity_str=None, empty_str=None):
    if self.is_empty:
        return empty_str or '()'

    start_str = '(' if self.start_open else '['
    end_str = ')' if self.end_open else ']'

    start = self.start
    end = self.end

    if start == end:
        x = start
        if callable(transformer):
            x = transformer(x)
        return '{}{}{}'.format(start_str, x, end_str)

    if self.is_negative_infinite:
        start = '-' + (infinity_str or 'inf')
    elif callable(transformer):
        start = transformer(start)

    if self.is_positive_infinite:
        end = infinity_str or 'inf'
    elif callable(transformer):
        end = transformer(end)

    return '{}{}, {}{}'.format(start_str, start, end, end_str)