Ranges

A utility for working with ranges

The basic building block of the module is the Range class.

Usage

A few examples:

from xocto.ranges import Range, RangeBoundaries
> > > Range(0, 2, boundaries=RangeBoundaries.EXCLUSIVE_INCLUSIVE)
> > > <Range: (0,2]>
> > > 0 in Range(0, 2)
> > > True
> > > 2 in Range(0, 2)
> > > False
> > > date(2020, 1, 1) in Range(date(2020, 1, 2), date(2020, 1, 5))
> > > False
> > > sorted([Range(1, 4), Range(0, 5)])
> > > [<Range: [0,5)>, <Range: [1,4)>]

See xocto.ranges for more details, including examples and in depth technical details.

API Reference

class xocto.ranges.FiniteDateRange(start: date, end: date)[source]

Bases: FiniteRange[date]

This subclass is a helper for a common usecase for ranges - representing finite intervals of whole days.

boundaries: RangeBoundaries
property days: int

Return the number of days between the start and end of the range.

end: T
intersection(other: Range[date]) FiniteDateRange | None[source]

Intersections with finite ranges will always be finite.

is_disjoint(other: Range[date]) bool[source]

Test whether the two ranges are disjoint.

start: T
union(other: Range[date]) FiniteDateRange | None[source]

Unions between two FiniteDateRanges should produce a FiniteDateRange.

class xocto.ranges.FiniteDatetimeRange(start: datetime, end: datetime)[source]

Bases: FiniteRange[datetime]

This subclass is a helper for a common usecase for ranges - representing finite intervals of time.

boundaries: RangeBoundaries
property days: int

Return the number of days between the start and end of the range.

end: T
intersection(other: Range[datetime]) FiniteDatetimeRange | None[source]

Intersections with finite ranges will always be finite.

property seconds: int

Return the number of seconds between the start and end of the range.

start: T
class xocto.ranges.FiniteRange(start: T | None, end: T | None, *, boundaries: str | RangeBoundaries = RangeBoundaries.INCLUSIVE_EXCLUSIVE)[source]

Bases: Range[T]

A FiniteRange represents a range that MUST have finite endpoints (i.e. they cannot be None).

This is mostly to add type checking, as we can specify that functions require a finite range and then skip checking if the endpoints are None.

boundaries: RangeBoundaries
end: T
intersection(other: Range[T]) FiniteRange[T] | None[source]

Intersections with finite ranges will always be finite.

start: T
class xocto.ranges.FiniteRangeSet(source_ranges: Iterable[Range[T]] | None = None)[source]

Bases: RangeSet[T]

This subclass is useful when dealing with finite intervals as we can offer stronger guarantees than we can get with normal RangeSets - mostly around intersections being finite etc.

intersection(other: Range[T] | RangeSet[T]) FiniteRangeSet[T][source]

Return the intersection of this RangeSet and the other Range or RangeSet.

pop() FiniteRange[T][source]

Remove and return an arbitrary set element.

Raises KeyError if the set is empty.

class xocto.ranges.HalfFiniteRange(start: T | None, end: T | None, *, boundaries: str | RangeBoundaries = RangeBoundaries.INCLUSIVE_EXCLUSIVE)[source]

Bases: Range[T]

This is also for type-checking, but represents a very common range type in Kraken (possibly the most common).

Specifically, ranges that MUST have a finite inclusive left endpoint and a (possibly infinite) exclusive right endpoint.

However LeftFiniteInclusiveRightExclusiveRange is a bit of a mouthful so we’re going with HalfFiniteRange.

boundaries: RangeBoundaries
end
intersection(other: Range[T]) HalfFiniteRange[T] | None[source]

Intersections with half finite ranges will always be half finite.

start: T
class xocto.ranges.HalfFiniteRangeSet(source_ranges: Iterable[Range[T]] | None = None)[source]

Bases: RangeSet[T], Generic[T]

This subclass is useful when dealing with half-finite intervals as we can offer stronger guarantees than we can get with normal RangeSets - mostly around intersections being half-finite etc.

intersection(other: Range[T] | RangeSet[T]) HalfFiniteRangeSet[T][source]

Return the intersection of this RangeSet and the other Range or RangeSet.

pop() HalfFiniteRange[T][source]

Remove and return an arbitrary set element.

Raises KeyError if the set is empty.

class xocto.ranges.Range(start: T | None, end: T | None, *, boundaries: str | RangeBoundaries = RangeBoundaries.INCLUSIVE_EXCLUSIVE)[source]

Bases: Generic[T]

The basic concept of an range of some sort of comparable item, specified by its endpoints and boundaries (whether the endpoints are inclusive or exclusive). This class provides some useful helpers for working with ranges.

Usage:

  • Ranges are declared by specifying their endpoints and boundaries. If the boundaries is omitted, then they are inclusive-exclusive by default

    >>> Range(0, 2, boundaries=RangeBoundaries.EXCLUSIVE_INCLUSIVE)
    <Range: (0,2]>
    >>> Range(0, 2, boundaries="[]")
    <Range: [0,2]>
    >>> r = Range(0, 2)
    >>> print(f"{r}")
    "[0,2)"
    
  • If an endpoint is set as None, then that means that the range is effectively infinite. Infinite ranges must have exclusive bounds for the infinite ends. We provide the continuum to the continnum helper to get an unbounded range.

    >>> Range(0, None)
    <Range: [0, None)
    >>> Range(0, None, boundaries="[]")  # Invalid
    >>> Range.continuum()  # Helper function
    <Range: (None, None)>
    
  • Ranges can be declared for any comparable type

    >>> int_erval: Range[int] = Range(0, 2)
    >>> date_erval: Range[date] = Range(date(2020, 1, 1), date(2020, 6, 6))
    >>> string_erval: Range[str] = Range("ardvark", "zebra")  # Uses lexical ordering
    
  • Ranges are themselves comparable. Two ranges are ordered by their start, with their end used to break ties

    >>> sorted([Range(1, 4), Range(0, 5)])
    [<Range: [0,5)>, <Range: [1,4)>]
    >>> sorted([Range(1, 2), Range(None, 2)])
    [<Range: [None,2)>, <Range: [1,2)>]
    >>> sorted([Range(3, 5), Range(3, 4)])
    [<Range: [3,4)>, <Range: [4,5)>]
    >>> sorted([Range(0, 2, boundaries=b) for b in RangeBoundaries])
    [<Range: [0,2)>, <Range: [0,2]>, <Range: (0,2)>, <Range: (0,2]>]
    
  • The in operator is provided to check if an item of the range type is within the the range

    >>> 0 in Range(0, 2)
    True
    >>> 2 in Range(0, 2)
    False
    >>> date(2020, 1, 1) in Range(date(2020, 1, 2), date(2020, 1, 5))
    False
    
  • The intersection function (which is aliased to the and (&) operator) will return the overlap of two ranges, or None if they are disjoint

    >>> Range(0, 2).intersection(Range(1, 4))
    <Range: [1,2)>
    >>> Range(1, 2) & Range(3, 4)
    None
    
  • The is_disjoint function will tell you if two ranges are disjoint
    >>> Range(0, 2).is_disjoint(Range(3, 5))
    True
    >>> Range(0, 2).is_disjoint(Range(2, 5))  # Since the default is not right-inclusive
    True
    
  • The union function (which is aliased to the or (|) operator), will return a range which covers two overlapping (or touching) ranges, or None if they are disjoint.

    >>> Range(0, 2).union(Range(1, 3))
    <Range: [0,3)>
    >>> Range(0, 2) | Range(2, 4)
    <Range: [0,4)>
    >>> Range(0, 2) | Range(3, 4)
    None
    >>> Range(0, 2) | Range(2, 4, boundaries="(]")
    None
    >>> Range(0, 2, boundaries="[]") | Range(3, 4)  # Since Range doesn't understand that 2 and 3 are adjacent.
    None
    
  • The difference function (which is aliased to the subtraction (-) operator), will return a range which contains the bit of this range which is not covered by the other range, or a rangeset which contains the bits of this range which are not covered (or None if the other range covers this one).

    >>> Range(0, 4) - Range(2, 4)
    <Range: [0,2)>
    >>> Range(0, 4) - Range(2, 3)
    <RangeSet: {[0,2), [3,4)}>
    >>> Range(0, 4) - Range(0, 5)
    None
    
boundaries: RangeBoundaries
classmethod continuum() Range[T][source]

Return a range representing the continnum.

difference(other: Range[T]) Range[T] | RangeSet[T] | None[source]

Return a range or rangeset consisting of the bits of this range that do not intersect the other range (or None if this range is covered by the other range).

end
intersection(other: Range[T]) Range[T] | None[source]

Return the intersection of the two ranges if it exists, or None if they are disjoint.

is_disjoint(other: Range[T]) bool[source]

Test whether the two ranges are disjoint.

is_finite() bool[source]
is_left_finite() bool[source]
is_right_finite() bool[source]
start
union(other: Range[T]) Range[T] | None[source]

If two ranges overlap (or are adjacent), return an range covering the two ranges. If the two ranges are disjoint, return None.

class xocto.ranges.RangeBoundaries(value)[source]

Bases: Enum

An enumeration.

EXCLUSIVE_EXCLUSIVE = '()'
EXCLUSIVE_INCLUSIVE = '(]'
INCLUSIVE_EXCLUSIVE = '[)'
INCLUSIVE_INCLUSIVE = '[]'
classmethod from_bounds(left_exclusive: bool, right_exclusive: bool) RangeBoundaries[source]

Convenience method to get the relevant boundary type by specifiying the exclusivity of each end.

class xocto.ranges.RangeSet(source_ranges: Iterable[Range[T]] | None = None)[source]

Bases: Generic[T]

A RangeSet represents an ordered set of disjoint ranges. It is constructed from an iterable of Ranges (which can be omited to create an empty RangeSet):

>>> RangeSet()  # empty RangeSet
<RangeSet: {}>
>>> rs = RangeSet([Range(0, 1), Range(2, 4)])  # Single iterable of ranges
>>> print(f"{rs}")
"{[0,1), [2, 4)}"

Overlapping Ranges are condensed when they are added to a set:

>>> RangeSet([Range(0, 3), Range(2, 4)]) == RangeSet([Range(0, 4)])
True
add(item: Range[T]) None[source]
complement() RangeSet[T][source]

Get a rangeset representing the ranges between the ranges in this rangeset and infinite left and right bounds.

contains_item(item: T) bool[source]

Check if an item is contained by any Range within this RangeSet.

contains_range(range: Range[T]) bool[source]

Check a Range is fully contained within another Range within this RanegSet.

difference(other: RangeSet[T]) RangeSet[T][source]

Return a rangeset consisting of the bits of this rangeset that do not intersect the other rangeset.

discard(item: Range[T]) None[source]

Discarding a range from a range set is equivalent to “cutting away” all the intersections of that range with the rangeset.

intersection(other: Range[T] | RangeSet[T]) RangeSet[T][source]

Return the intersection of this RangeSet and the other Range or RangeSet.

is_disjoint(other: RangeSet[T]) bool[source]

Check whether this RangeSet is disjoint from the other one.

is_finite() bool[source]
is_left_finite() bool[source]
is_right_finite() bool[source]
pop() Range[T][source]

Remove and return an arbitrary set element.

Raises KeyError if the set is empty.

union(other: RangeSet[T]) RangeSet[T][source]
xocto.ranges.any_overlapping(ranges: Iterable[Range[T]]) bool[source]

Return true if any of the passed Ranges are overlapping.

xocto.ranges.as_finite_datetime_periods(periods: Iterable[HalfFiniteRange[datetime] | Range[datetime]]) Sequence[FiniteDatetimeRange][source]

Casts the given date/time periods as finite periods.

This is useful when working with potentially infinite ranges that are known to be finite e.g. due to intersection with a finite range.

Raises:

ValueError: If one or more periods is not finite.

xocto.ranges.get_finite_datetime_ranges_from_timestamps(finite_datetime_range: FiniteRange[datetime], timestamps: Iterable[datetime]) Sequence[FiniteDatetimeRange][source]

Given a datetime range and some timestamps, cut that period into multiple points whenever one of the timestamps falls within it.

Sorts and deduplicates the timestamps first.

Example:

  • Input:
    • period: (“2021-09-01 00:00:00”, “2021-10-01 00:00:00”)

    • timestamps: [“2021-09-10 00:00:00”, “2021-09-16 00:00:00”, “2021-09-23 00:00:00”]

  • Return:
    [

    (“2021-09-01 00:00:00”, “2021-09-10 00:00:00”), (“2021-09-10 00:00:00”, “2021-09-16 00:00:00”), (“2021-09-16 00:00:00”, “2021-09-23 00:00:00”), (“2021-09-23 00:00:00”, “2021-10-01 00:00:00”),

    ]